Русский
Русский
English
Статистика
Реклама

Android studio

Compose. Jetpack Compose

09.10.2020 12:10:51 | Автор: admin
image

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

Пожалуй, главным трендом мобильной разработки за последние несколько лет стал декларативный UI. Такое решение уже давно успешно применяется в веб и кроссплатформенных решениях и, наконец, добралось и до нативной разработки. На iOS существует SwiftUI (представленный на WWDC 2019), а на Android Jetpack Compose (представленный месяцем ранее на Google I/O 2019). И именно о последнем мы сегодня и поговорим.

Примечание: в данной статье мы не будем рассматривать поэтапное создание первого проекта на Compose, так как этот процесс прекрасно описан в других материалах. Моя цель лишь рассказать о преимуществах и недостатках, которые дает android-разработчикам переход на Jetpack Compose, а решение использовать или нет всегда остаётся за вами.

Появление


Официальная история Jetpack Compose начинается с мая 2019, когда он был представлен публике на конференции Google I/O. Простой, реактивный и Kotlin-only новый декларативный фреймворк от Google выглядел как младший брат Flutter (который к тому моменту уже стремительно набирал популярность).

API design is building future regret

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

Преимущества


Итак, чем же хорош Jetpack Compose и, главное, чем он кардинально отличается от существующего на данный момент UI-фреймворка Android?

  • Unbundled toolkit: JC не зависит от конкретных релизов платформы, а значит, забудем уже про Support Library.
  • Kotlin-only: Больше не нужно переключаться между классами и xml-файлами вся работа с UI происходит в одном Kotlin-файле.
  • Композитный подход: Наследованию нет, композиции да. Каждый UI-компонент представляет собой обычную composable-функцию, отвечающую только за ограниченный функционал, т.е. без лишней логики. Никаких больше View.java на 30 тысяч строк кода.
  • Unidirectional Data Flow: Одна из основополагающих концепций Jetpack Compose, о которой будет рассказано подробнее чуть ниже.
  • Обратная совместимость: Для использования Compose не требуется начинать проект с нуля. Имеется возможность как его встраивания (с помощью ComposeView) в имеющуюся xml-вёрстку, так и наоборот.
  • Меньше кода: Тут, как говорится, лучше один раз увидеть, чем сто раз услышать. В качестве примера возьмём классическое сочетание компонентов два поля ввода и кнопка подтверждения:

В реализации текущего UI-фреймворка вёрстка этих компонентов выглядит так:

<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://personeltest.ru/away/schemas.android.com/apk/res/android"    android:layout_width="match_parent"    android:layout_height="wrap_content"    xmlns:app="http://personeltest.ru/away/schemas.android.com/apk/res-auto"    android:orientation="vertical"    android:padding="@dimen/padding_16dp">    <com.google.android.material.textfield.TextInputLayout        android:id="@+id/til_login"        android:layout_width="match_parent"        android:layout_height="wrap_content"        style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"        android:hint="@string/sign_in_email"        android:layout_marginBottom="@dimen/margin_8dp">        <com.google.android.material.textfield.TextInputEditText            android:id="@+id/et_login"            android:layout_width="match_parent"            android:layout_height="wrap_content"            android:inputType="text"/>    </com.google.android.material.textfield.TextInputLayout>    <com.google.android.material.textfield.TextInputLayout        android:id="@+id/til_password"        android:layout_width="match_parent"        android:layout_height="wrap_content"        style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"        android:hint="@string/sign_in_password"        android:layout_marginVertical="@dimen/margin_8dp">        <com.google.android.material.textfield.TextInputEditText            android:id="@+id/et_password"            android:layout_width="match_parent"            android:layout_height="wrap_content"            android:inputType="textPassword"/>    </com.google.android.material.textfield.TextInputLayout>    <Button        android:id="@+id/btn_confirm"        android:layout_width="match_parent"        android:layout_height="wrap_content"        android:text="@string/sign_in_submit"        android:layout_marginTop="@dimen/margin_8dp"        android:padding="@dimen/padding_8dp"        android:background="@color/purple_700"/></LinearLayout>

В то же время, при использовании Jetpack Compose, решение будет выглядеть следующим образом:

@Preview@Composablefun LoginPage(){    var loginValue by remember { mutableStateOf(TextFieldValue("")) }    var passwordValue by remember { mutableStateOf(TextFieldValue("")) }    Surface(color = Color.White) {        Column(modifier = Modifier.padding(16.dp).fillMaxWidth()) {            Surface(color = Color.White, modifier = Modifier.padding( vertical = dimensionResource(id = R.dimen.padding_8dp))) {                OutlinedTextField(                        value = loginValue,                        onValueChange = { loginValue = it },                        label = { Text(text = stringResource(id = R.string.sign_in_email)) },                        placeholder = { Text(text = stringResource(id = R.string.sign_in_email)) },                        modifier = Modifier.fillMaxWidth()                )            }            Surface(color = Color.White, modifier = Modifier.padding( vertical = dimensionResource(id = R.dimen.padding_8dp))) {                OutlinedTextField(                        value = passwordValue,                        onValueChange = { passwordValue = it },                        label = { Text(text = stringResource(id = R.string.sign_in_password)) },                        placeholder = { Text(text = stringResource(id = R.string.sign_in_password)) },                        visualTransformation = PasswordVisualTransformation(),                        modifier = Modifier.fillMaxWidth()                )            }            Button(                    onClick = {},                    modifier = Modifier.padding( vertical = dimensionResource(id = R.dimen.padding_8dp)).fillMaxWidth(),                    backgroundColor = colorResource(R.color.purple_700)) {                Text(text = stringResource(id = R.string.sign_in_submit), modifier = Modifier.padding(8.dp))            }        }    }}

Ну и напоследок сравнительный результат:

image

Недостатки


  • Alpha-версия: Безусловно, более чем за год разработки фреймворк значительно преобразился и стал гораздо стабильнее. Однако это всё ещё альфа, а поэтому за пределами Pet-проектов использовать его не рекомендуется.

Декларативный стиль


Отдельное внимание стоит уделить главной особенности Jetpack Compose декларативному стилю создания UI. Суть подхода заключается в описании интерфейса как совокупности composable-функций (они же виджеты), которые не используют под капотом view, а напрямую занимаются отрисовкой на canvas. Для кого-то это минус, для других возможность попробовать что-то новое. Так или иначе, к концепции верстать UI кодом нативному разработчику, не работавшему ранее с аналогичными технологиями (к примеру, Flutter или React Native), придётся привыкать.

Что за Unidirectional Data Flow?


В современном android-приложении UI-состояние меняется в зависимости от приходящих событий (нажатие на кнопку, переворот экрана и т.д.). Мы нажимаем на компонент, тем самым формируя событие, а компонент меняет свой state и вызывает callback в ответ. Из-за довольно тесной связи UI-состояния с View это потенциально может привести к усложнению поддержки и тестирования такого кода. К примеру, возможна ситуация, когда помимо внутреннего state компонента, мы можем хранить его состояние в поле (например во viewmodel), что теоретически может привести к бесконечному циклу обновления этого самого state.

Что же касается Jetpack Compose, то здесь все компоненты по умолчанию являются stateless. Благодаря принципу однонаправленности нам достаточно скормить модель данных, а любое изменение состояния фреймворк обработает за нас. Таким образом, логика компонента упрощается, а инкапсуляция состояния позволяет избежать ошибок, связанных с его частичным обновлением. В качестве примера возьмем уже рассмотренный ранее composable-код. Перед описание компонентов были определены две переменные:

    var loginValue by remember { mutableStateOf(TextFieldValue("")) }    var passwordValue by remember { mutableStateOf(TextFieldValue("")) }

Мы создаем два текстовых объекта, значения которых будем устанавливать полям ввода (логина и пароля) в качестве value. А благодаря связке remember { mutableStateOf() } любое изменение значений этих объектов (из других частей кода) уведомит об этом соответствующее поле ввода, которое перерисует только значение value, вместо полной рекомпозиции всего компонента.

Вывод


Какой же вывод можно сделать о Jetpack Compose? По моему мнению, у нового решения от Google имеется огромный потенциал. С момента анонса в 2019 году была проделана огромная работа, и не менее долгий путь до релиза у фреймворка ещё впереди. Однако теперь он публично доступен, и я считаю, что это прекрасная возможность познакомиться с ним поближе. Ну а за чем, по вашему мнению, будущее пишите в комментарии, будет интересно узнать ваше мнение. Любите android!
Подробнее..

Перевод Меняем стандартный диалог сбоя приложения в Android на собственный экран

01.12.2020 18:05:13 | Автор: admin

Как вместо стандартного диалогового окна о сбое использовать собственный экран с сообщением об ошибке

Будущих учащихся на курсе "Android Developer. Basic" приглашаем посетить открытый вебинар на тему "Приложение под Android на Kotlin за 1,5 часа"


А пока делимся традиционным полезным переводом.


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

Живи так, как будто ты умрешь завтра. Учись так, как будто ты будешь жить вечно.

Мне не нравится стандартное диалоговое окно, которое отображается при сбое приложения. Поэтому я нашел интересную библиотеку, которая позволяет отображать собственный экран с сообщением об ошибке вместо стандартного диалогового окна.

Давайте разберемся, как это сделать.

Что нам потребуется?

  1. Kotlin

  2. Android Studio

  3. Та самая библиотека.

После создания нового проекта добавьте в файл build.gradle(:app) следующую зависимость:

implementation 'cat.ereza:customactivityoncrash:2.3.0'

Создадим макет собственного экрана сбоя. Например, такой:

Макет собственного экрана сбояМакет собственного экрана сбоя

Эту картинку, изображающую сбой, я скачал отсюда.

<?xml version="1.0" encoding="utf-8"?><RelativeLayout xmlns:android="http://personeltest.ru/away/schemas.android.com/apk/res/android"    xmlns:app="http://personeltest.ru/away/schemas.android.com/apk/res-auto"    xmlns:tools="http://personeltest.ru/away/schemas.android.com/tools"    android:layout_width="match_parent"    android:layout_height="match_parent">    <com.google.android.material.textview.MaterialTextView        android:id="@+id/errorText"        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:layout_centerHorizontal="true"        android:layout_marginTop="30dp"        android:fontFamily="sans-serif"        android:text="@string/ouch_something_went_wrong"        android:textAppearance="@style/TextAppearance.AppCompat.Large"        android:textStyle="bold" />    <androidx.appcompat.widget.AppCompatImageView        android:layout_width="match_parent"        android:layout_height="match_parent"        android:src="@drawable/ic_error_img" />    <com.google.android.material.button.MaterialButton        android:id="@+id/restartApp"        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:layout_alignParentBottom="true"        android:layout_centerHorizontal="true"        android:layout_marginBottom="50dp"        android:text="Restart App"        android:textAllCaps="false" /></RelativeLayout>

Как стать успешным разработчиком для Android

Следуйте этому плану, если хотите стать профессиональным разработчиком для Android

medium.com

Давайте добавим код, который отслеживает событие сбоя и отображает собственный экран сбоя вместо стандартного диалогового окна Android.

package com.example.customerrorscreenimport android.os.Bundleimport android.view.Viewimport androidx.appcompat.app.AppCompatActivityimport cat.ereza.customactivityoncrash.CustomActivityOnCrashimport kotlinx.android.synthetic.main.activity_error.*class ErrorActivity : AppCompatActivity() {    override fun onCreate(savedInstanceState: Bundle?) {        super.onCreate(savedInstanceState)        setContentView(R.layout.activity_error)        /** you can log the reason of crash by adding this line of code         *  CustomActivityOnCrash.getStackTraceFromIntent(intent)         */        //getting crashing intent        val config = CustomActivityOnCrash.getConfigFromIntent(intent)        /**         * If config is null or not getting an intent simply finish the app         */        if (config == null) {            finish()            return        }                if (config.isShowRestartButton && config.restartActivityClass != null) {            restartApp.text = "Restart App"            restartApp.setOnClickListener {                CustomActivityOnCrash.restartApplication(                    this,                    config                )            }        } else {            restartApp.text = "Close App"            restartApp.setOnClickListener {                CustomActivityOnCrash.closeApplication(                    this,                    config                )            }        }    }}

Нужно создать класс приложения, который будет зарегистрирован в манифесте Android с целью глобальной регистрации библиотеки обработки сбоев в приложении.

package com.example.customerrorscreenimport android.app.Applicationimport cat.ereza.customactivityoncrash.config.CaocConfig/** * Created by  Mustufa on 04/11/2020. * Email : mustufaayub82@gmail.com */class MyApp : Application() {    override fun onCreate() {        super.onCreate()        CaocConfig.Builder.create()            .trackActivities(true)            .errorActivity(ErrorActivity::class.java)            .apply()    }}

Добавим этот класс в файл AndroidManifest.xml, используя атрибут name.

AndroidManifest.xmlAndroidManifest.xml

Теперь перейдем к файлу MainActivity.kt и поэкспериментируем с нашей новой возможностью. Мы должны вызвать сбой в приложении, чтобы увидеть наш экран сбоя. Я добавлю код, вызывающий сбой приложения, в метод onCreate.

var array = mutableListOf<String>()array[0] = "Hello"findViewById<TextView>(R.id.textView).text = array[1]

При выполнении этого кода будет сгенерировано исключение IndexOutOfBoundException и в приложении возникнет сбой.

Запустим приложение.

Исходный код можно скачать отсюда.


Узнать подробнее о курсе "Android Developer. Basic".

Записаться на открытый урок на тему "Приложение под Android на Kotlin за 1,5 часа".

Подробнее..

Как написать простое Android ToDo-приложение на Java

15.03.2021 22:22:10 | Автор: admin

Предисловие

Я не являюсь профессиональным разработчиком с огромным стажем в данной области (и это даже не хобби, а лишь нужда в разработке конкретного приложения), потому данная статья, полагаю, будет полезна новичкам, таким же, как и я был в начале разработке своего приложения. Возможно, кто-то найдет что-то полезное из данной статьи, какие-то кусочки окажутся частью ваших будущих разработок.

Я расскажу вам как написать простенькое ToDo-приложение на Android с тремя активностями (рабочими экранами).

Ссылка на проект на Github будет в конце данной статьи.

Установка и первичная настройка

Для разработки приложения я рассмотрю использование бесплатной IDEIntellij от разработчиков JetBrains - Android Studio, у меня версия 4.1.1.

После успешной установки IDE и запуска нажимаем на самую первую кнопкуStart a new Android Studio Project. Далее появится мастер первичной подготовки проекта:

  • выберем подходящий шаблон, в моем случае это Empty Activity - он самый простой для новичков, так как при первом запуске будет всего 1 XML файл с версткой и один java файл MainActivity.

  • На следующем экране придумываем имя приложению; помните, что package name, после публикации на Google Play изменить нельзя (иначе Google Play посчитает это другим приложением (поправьте меня, если я ошибаюсь). Выбираем язык Java, так как по нему данная статья, а также, по нему больше информации в Интернете, чем по Kotlin.

  • Минимальный SDK выбираем под Android 5.0, так как данного API будет предостаточно для наших задач, заодно мы получим большой охват, в том числе старых устройств: планшеты, смартфоны, встроенные системы.

Скриншоты: установка и первичная настройка

Далее раскрываем вкладку Project и находим в каталоге Java><Ваш_Проект> файл MainActivity.java, в котором мы будем описывать все происходящее на главном экране.

Подготовка макетов (layouts) - внешний облик приложения

После рассмотрим файл MainActivity.xml, для этого нам нужно найти каталог res>layout>. Откроем MainActivity.xml для создания облика первой - главной страницы и перетягивая спанели Palette необходимые нам типы объектов.

Советую вам размещать объекты под ConstraintLayout, так объекты можно будет привязывать узелками к родительскому ConstraintLayout, который по умолчанию занимает всю пространство, а после привязки узелков, мы можем размещать объекты на нужном нам вертикальном и горизонтальном выравнивании.

Кстати, вместо px, тут используется другая величина - dp, позволяющая на разных экранах видеть одинаковый и желаемый результат.

Кстати, также, советую названия Текст полей переназначать в String значения, чтобы в дальнейшем было проще делать перевод интерфейса - подобный функционал уже встроен в Android Studio. Для этого нажимаем на объект, далее в меню Свойств объекта находим поле text и нажимаем на маленькую плашку-кнопку справа от текста. В открывшимся окне, нажимаем на плюсик слева сверху и создаем название String-переменной и ее значение по умолчанию:

Создание String-переменнойСоздание String-переменной

Для перевода интерфейса, необходимо сохранить изменения и над нашим конструктором Layout нажать на кнопку Default (en-us) и выбрать Edit Translations, далее найти слева сверху значок глобуса и нажать на него для добавления нового языка:

Переводы для интерфейсовПереводы для интерфейсов

Таким образом создадим дополнительные макеты (layouts) для оставшихся двух окон:

Скриншоты: еще два макета
Макет Activity_Settings.xmlМакет Activity_Settings.xmlМакет Activity_Advanced.xmlМакет Activity_Advanced.xml

Программируем на Java под Android

Еще раз повторюсь, что это Tutorial больше для новичков; дальше я буду комментировать практически каждую строчку. Ссылка на проект на Github будет в конце данной статьи.

Открываем файл Main_Activity.java, который будет отвечать за логику наших переключателей и главного экрана в целом, а она такова:

  • В самом верху должен отображаться пользовательский заголовок, если он настроен.

  • На переключателях должен отображаться тот текст, который пользователь настраивает из окна с макетом Activity_Settings.xml

  • Количество переключателей должно соответствовать заданному числу из окна макета Activity_Advanced.xml

  • После выхода из приложения и повторного запуска все переключатели должны оставаться в том же положении, в котором пользователь их оставил

  • Сброс переключателей возможен только, если переключатель Уверен/-а? включен.

  • А также, должны работать оставшиеся кнопки меню.


    Пишем следующее:

Код под спойлером: 156 строчек
package com.bb.myapplication;import androidx.appcompat.app.AppCompatActivity;import androidx.appcompat.widget.SwitchCompat;import android.content.Intent;import android.content.SharedPreferences;import android.graphics.Color;import android.os.Bundle;import android.view.View;import android.widget.Button;import android.widget.TextView;public class MainActivity extends AppCompatActivity {    //Создаем 9 переключателей с помощью массива    SwitchCompat[] switcharray = new SwitchCompat[9];    Boolean Reset; //Булев для выключения переключателя    Button NextButton;    public int[] list_of_switches = {            R.id.switch_compat1,            R.id.switch_compat2,            R.id.switch_compat3,            R.id.switch_compat4,            R.id.switch_compat5,            R.id.switch_compat6,            R.id.switch_compat7,              R.id.switch_compat8,             R.id.switch_compat10, //переключатель "Вы уверены?" //8    };    //Нажатие кнопки Сброс    public void ResetButtonClick (View view) throws IllegalAccessException {        Reset =false;        if (switcharray[8].isChecked()) { //Если переключатель "Вы уверены?" нажат, то разрешаем переключить в false остальные переключатели            SharedPreferences.Editor editor = getSharedPreferences("save"                    ,MODE_PRIVATE).edit();            //Сохраняем в Intent значения всех переключателей в False            for (int k=0; k<10; k++) {                editor.putBoolean("value"+k, false);            }            editor.apply();            //Устанавливаем все переключатели в значение False            for (int i=0;i<9;i++){                switcharray[i].setChecked(false);            }            //Reset background color of checked SwitchCompats            for (int i = 0; i < 9; i++) {                findViewById(list_of_switches[i]).setBackgroundColor(Color.TRANSPARENT);            }        }    }    //Создание формы / открытие приложения    @Override    protected void onCreate(final Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        //Назначаем полям значения по умолчанию и сохраняем их в Intent        String[] tsfield = new String[8];        SharedPreferences prefs = getSharedPreferences("MY_DATA", MODE_PRIVATE);        tsfield[0] = prefs.getString("KEY_F0", "Выключил газ");        tsfield[1] = prefs.getString("KEY_F1", "Выключил воду");        tsfield[2] = prefs.getString("KEY_F2", "Покормил кошек");        tsfield[3] = prefs.getString("KEY_F3", "Закрыл окна");        tsfield[4] = prefs.getString("KEY_F4", "Выключил Интернет");        tsfield[5] = prefs.getString("KEY_F5", "Закрыл дверь");        tsfield[6] = prefs.getString("KEY_F6", "Выключил везде свет");        tsfield[7] = prefs.getString("KEY_F7", "Вынес мусор");        //Получаем настройки текста заголовка        String hellotext = prefs.getString("hellotitletext", "");        switcharray[6] = findViewById(list_of_switches[6]);        switcharray[7] = findViewById(list_of_switches[7]);        //Получаем настройки количества полей        String sixfields = prefs.getString("sixfields", "true");        String sevenfields = prefs.getString("sevenfields", "false");        String eightfields = prefs.getString("eightfields", "false");        if (sixfields.equals("true")){            switcharray[6].setVisibility(View.GONE);            switcharray[7].setVisibility(View.GONE);        }        else if (sevenfields.equals("true")) {            switcharray[6].setVisibility(View.VISIBLE);            switcharray[7].setVisibility(View.GONE);        }        else if (eightfields.equals("true")) {            switcharray[6].setVisibility(View.VISIBLE);            switcharray[7].setVisibility(View.VISIBLE);        }        //Создаем массив из TextView        TextView[] textarr = new TextView[8];        //Каждому переключателю назначаем текст из итерации поля tsfield        for (int i=0; i<8;i++){            textarr[i] = (TextView) findViewById(list_of_switches[i]);            textarr[i].setText(tsfield[i]);        }        //Назначаем текст заголовка            TextView textView5 = (TextView) findViewById(R.id.textView5);            textView5.setText(hellotext);        //Отображать заголовок, если соотв. поле заполнено        if(!hellotext.matches(""))        {            textView5.setVisibility(View.VISIBLE);        }        //Создаем связь каждого элемента переключателя по id из XML с соответствующей переменной типа SwitchCompat        for (int i=0;i<9;i++) {            switcharray[i] = findViewById(list_of_switches[i]);        }        //Создаем связь кнопки по id bt_next из xml переменной NextButton        NextButton = findViewById(R.id.bt_next);        //Используем SharedPreferences = "save"        SharedPreferences sharedPreferences = getSharedPreferences("save"                , MODE_PRIVATE);        //При первом запуске - все переключатели в False        for (int k=0; k<9; k++) {        switcharray[k].setChecked(sharedPreferences.getBoolean("value"+k, false));        }        //При переключении переключателей сохраняем данные, а также, проверяем их при повторном запуске        for (int k=0; k<9; k++) {            final int finalK = k;            switcharray[k].setOnClickListener(new View.OnClickListener() {                @Override                public void onClick(View view) {                    if (switcharray[finalK].isChecked()) {                        //когда переключатель включен                        Reset = true; //                        switcharray[finalK].setBackgroundColor(Color.parseColor("#c8a2c6"));                        SharedPreferences.Editor editor = getSharedPreferences("save"                                , MODE_PRIVATE).edit();                        editor.putBoolean("value" + finalK, true);                        editor.apply();                        switcharray[finalK].setChecked(true);                    } else {                        //когда переключатель выключен                        SharedPreferences.Editor editor = getSharedPreferences("save"                                , MODE_PRIVATE).edit();                        editor.putBoolean("value" + finalK, false);                        Reset = false;                        switcharray[finalK].setBackgroundColor(Color.TRANSPARENT);                        editor.apply();                        switcharray[finalK].setChecked(false);                    }                }            });        }        //Кнопка открытия страницы настроек        NextButton.setOnClickListener(new View.OnClickListener() {            @Override            public void onClick(View view) {                //Go to next activity                Intent intent2 = new Intent(MainActivity.this, Activity_settings.class);                startActivity(intent2);            }        });    }}

Следующим этапом будет написание кода для корректной работы макета Activity_Settings.XML, а логика его такова:

  • Введенные пользователь записи сохраняются даже после перезапуска приложения

  • Количество полей соответствуют числу, заданному в настройках из макета Activity_Advanced.xml

  • А также, должны работать оставшиеся кнопки меню.

Код по спойлером: 124 строчки
package com.bb.myapplication;import androidx.appcompat.app.AppCompatActivity;import android.content.Intent;import android.content.SharedPreferences;import android.net.Uri;import android.os.Bundle;import android.view.View;import android.widget.Button;import android.widget.EditText;import android.widget.TextView;public class Activity_settings extends AppCompatActivity {    //Initialize Variable    Button btBack;    Button fcSubmit;    Button btAdvanced;    //Ассоциируем поля ввода с переменными с помощью массива    EditText[] InputFields = new EditText[8];    //Назначаем полям значения по умолчанию и сохраняем их в Intent    String[] tsfield = new String[8];    //Создаем массив элементов из XML по id    public int[] list_of_fields = {            R.id.inputField0,            R.id.inputField1,            R.id.inputField2,            R.id.inputField3,            R.id.inputField4,            R.id.inputField5,            R.id.inputField6,            R.id.inputField7,    };    private SharedPreferences prefs;    //Создание формы / открытие приложения    @Override    public void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_settings);        //Назначаем полям значения по умолчанию и сохраняем их в Intent        prefs = getSharedPreferences("MY_DATA", MODE_PRIVATE);        //Получаем данные по количеству используемых полей        String sixfields = prefs.getString("sixfields", "true");        String sevenfields = prefs.getString("sevenfields", "false");        String eightfields = prefs.getString("eightfields", "false");        EditText inputField71var = (EditText) findViewById(list_of_fields[6]);        EditText inputField81var = (EditText) findViewById(list_of_fields[7]);        if (sixfields.equals("true")){            inputField71var.setVisibility(View.INVISIBLE);            inputField81var.setVisibility(View.INVISIBLE);        }        else if (sevenfields.equals("true")) {            inputField71var.setVisibility(View.VISIBLE);            inputField81var.setVisibility(View.INVISIBLE);        }        else if (eightfields.equals("true")) {            inputField71var.setVisibility(View.VISIBLE);            inputField81var.setVisibility(View.VISIBLE);        }        tsfield[0] = prefs.getString("KEY_F0", "Выключил газ");        tsfield[1] = prefs.getString("KEY_F1", "Выключил воду");        tsfield[2] = prefs.getString("KEY_F2", "Покормил кошек");        tsfield[3] = prefs.getString("KEY_F3", "Закрыл окна");        tsfield[4] = prefs.getString("KEY_F4", "Выключил Интернет");        tsfield[5] = prefs.getString("KEY_F5", "Закрыл дверь");        tsfield[6] = prefs.getString("KEY_F6", "Выключил везде свет");        tsfield[7] = prefs.getString("KEY_F7", "Вынес мусор");        //Назначаем полям ввода текст из SharedPreferences        for (int i=0; i<8; i++) {            InputFields[i] = (EditText) findViewById(list_of_fields[i]);            InputFields[i].setText(tsfield[i]);        }        //Создаем переменные для кнопок        btBack = findViewById(R.id.bt_back);        fcSubmit = findViewById(R.id.submit_fc);        btAdvanced = findViewById(R.id.btAdvanced);        //Кнопка Назад        btBack.setOnClickListener(new View.OnClickListener() {            @Override            public void onClick(View view) {                //Go back                Intent intent = new Intent (                        Activity_settings.this,MainActivity.class                );                startActivity(intent);            }        });        //Кнопка Расширенные настройки/Дополнительно        btAdvanced.setOnClickListener(new View.OnClickListener() {            @Override            public void onClick(View view) {                //Open Advanced Settings                Intent intent = new Intent (                        Activity_settings.this,Activity_advanced.class                );                startActivity(intent);            }        });    }    //Ссылка-значок на внешний ресурс - ссылка на мой телеграм    public void tglink(View view){        Intent myWebLink = new Intent(android.content.Intent.ACTION_VIEW);        myWebLink.setData(Uri.parse("https://t.me/EndlessNights"));            startActivity(myWebLink);    }    //Кнопка Сохранить данные    public void SaveData(View view)    {        for (int i=0; i<8;i++) {            tsfield[i] = InputFields[i].getText().toString();        SharedPreferences.Editor editor = prefs.edit();        editor.putString("KEY_F"+i, tsfield[i]);            editor.apply();        }        // Открываем главную страницу        startActivity(new Intent(getApplicationContext(), MainActivity.class));    }}

И наконец опишем логику работы последнего окна в приложении - с Дополнительными настройками:

  • Количество полей для отображения - в данном случае выбор с помощью радиокнопок - 6, 7 или 8 полей.

  • Текстовый заголовок, который пользователь может ввести и который будет отображаться на главной странице/активности.

  • Также, текстовый заголовок и выбранное количество полей с помощью радиокнопок должны сохранять свое состояние даже после перезапуска приложения.

  • И наконец должны работать оставшиеся кнопки меню.

Код под спойлером: 134 строчки
package com.bb.myapplication;import androidx.appcompat.app.AppCompatActivity;import android.content.Intent;import android.content.SharedPreferences;import android.os.Bundle;import android.view.View;import android.widget.Button;import android.widget.CompoundButton;import android.widget.EditText;import android.widget.RadioButton;import android.widget.RadioGroup;import android.widget.Switch;import android.widget.TextView;public class Activity_advanced extends AppCompatActivity {    Button btBack;    //Назначаем радиокнопкам значения по умолчанию    Boolean sixbool = true;    Boolean sevenbool = false;    Boolean eightbool = false;    private SharedPreferences prefsadv;    //Поле ввода текста для заголовка    private EditText hellotitletext;    RadioGroup rdGroup;    //Переменные для радиокнопок    public RadioButton r1, r2, r3;    //Переменные для передачи состояния из boolean в sharedPrefs    String sixdata;    String sevendata;    String eightdata;    Switch bgswitchvar;    private SharedPreferences prefs;    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_advanced);        bgswitchvar = findViewById(R.id.bgswitch);        prefsadv = getSharedPreferences("MY_DATA", MODE_PRIVATE);        rdGroup = (RadioGroup)findViewById(R.id.radioGroup);        //Поле заголовка        String hellotitletext1 = prefsadv.getString("hellotitletext","");        hellotitletext = (EditText) findViewById(R.id.hellotitletext);        hellotitletext.setText(hellotitletext1);        //Ассоциируем переменные с полями по id из xml        r1 = findViewById(R.id.sixfields);        r2 = findViewById(R.id.sevenfields);        r3 = findViewById(R.id.eightfields);        //При нажатии на радиокнопку, вызываем функцию Update с заданным ключом        r1.setChecked(Update("rbsix"));        r2.setChecked(Update("rbseven"));        r3.setChecked(Update("rbeight"));        //При нажатии первой кнопки добавляем True с ключом rbsix в RBDATA        r1.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {            @Override            public void onCheckedChanged(CompoundButton compoundButton, boolean r1_isChecked) {                SaveIntoSharedPrefs("rbsix", r1_isChecked);            }        });        //При нажатии второй кнопки добавляем True с ключом rbsix в RBDATA        r2.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {            @Override            public void onCheckedChanged(CompoundButton compoundButton, boolean r2_isChecked) {                SaveIntoSharedPrefs("rbseven", r2_isChecked);            }        });        //При нажатии третьей кнопки добавляем True с ключом rbsix в RBDATA        r3.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {            @Override            public void onCheckedChanged(CompoundButton compoundButton, boolean r3_isChecked) {                SaveIntoSharedPrefs("rbeight", r3_isChecked);            }        });        //Back button        btBack = findViewById(R.id.btBackadvanced);        btBack.setOnClickListener(new View.OnClickListener() {            @Override            public void onClick(View view) {                //Go back                Intent intent = new Intent (                        Activity_advanced.this,Activity_settings.class                );                startActivity(intent);            }        });    }    //Сохранение данных в SharedPreferences - ожидая ключ и значение булева типа    private void SaveIntoSharedPrefs(String key, boolean value){        SharedPreferences sp = getSharedPreferences("RBDATA",MODE_PRIVATE);        SharedPreferences.Editor editor = sp.edit();        editor.putBoolean(key,value);        editor.apply();    }    //Функция обновления значения в SharedPreferences    private boolean Update(String key){        SharedPreferences sp = getSharedPreferences("RBDATA",MODE_PRIVATE);        return sp.getBoolean(key, false);    }    //Сохраняем данные по количеству полей    public void SaveDataAdvanced(View view)    {        int checkedId = rdGroup.getCheckedRadioButtonId();        if(checkedId == R.id.sixfields) {            sixbool = true;            sevenbool = Boolean.FALSE;            eightbool = Boolean.FALSE;        }        else if (checkedId == R.id.sevenfields){            sevenbool = true;            sixbool = Boolean.FALSE;            eightbool = Boolean.FALSE;        }        else if (checkedId == R.id.eightfields){            eightbool = true;            sevenbool = Boolean.FALSE;            sixbool = Boolean.FALSE;        }        sixdata = String.valueOf(sixbool);        sevendata = String.valueOf(sevenbool);        eightdata = String.valueOf(eightbool);        String hellofield = hellotitletext.getText().toString();        SharedPreferences.Editor editor = prefsadv.edit();        editor.putString("sixfields", sixdata);        editor.putString("sevenfields", sevendata);        editor.putString("eightfields", eightdata);        editor.putString("hellotitletext", hellofield);        editor.apply();        startActivity(new Intent(getApplicationContext(), MainActivity.class));    }}

Подготовка приложения к публикации

Для отладки и проверки работоспособности приложения советую вам использовать настоящее устройство на Android, так вы сразу сможете отследить наличие, как минимум проблем с оформлением.

Здесь я приложил видео-инструкцию, как подключить свой смартфон к Android studio для отладки вашего приложения. На видео вы можете заметить первую версию данного приложения с очень плохим кодом:

Регистрация в Google Play

Для публикации приложения нам следует создать специальный аккаунт разработчика, вот прямая ссылка.

Далее вам предстоит оплатить пошлину в $35 за возможность публиковать приложения, это почти в 3 раза дешевле, чем в Steam, при том, что Steam просит $100 за каждое публикуемое приложение/игру, даже бесплатное, а с аккаунтом разработка, в Google Play вы можете публиковать несчётное множество приложений.

После оплаты и успешной авторизации в консоли разработчика, необходимо нажать на синюю кнопку "Создать приложение", далее заполнить все необходимые поля:

После создания приложения в консоли разработчика Google Play, необходимо перейти в раздел Рабочая версия и нажать на кнопку Создать новый выпуск. Вам предложат получить электронную подпись для вашего приложения с расширением *.jks, с помощью которой вам предстоит подписать свое первое приложение, а также, все дальнейшие выпуски с обновлениями.

Возвращаемся в Android Studio и необходимо заполнить немного информации о нашем приложении, для этого нажимаем File>Project Structure и заполняем поля Version Code и Version Name - без них Google Play Google Play не допустит ваше приложение до публикации:

Наконец, переходим в следующий раздел: пункт меню Build>Generate Signed Bundle / APK

В открывшимся окне выбираем APK. В подразделе Key Store Path выбираем Create new, далее заполняем все поля (прямая ссылка на официальную инструкцию), далее данный ключ потребуется загрузить в консоль Google Play. Затем вернемся в Android Studio и после ввода всех необходимых данных, нажимаем Next

В следующем окне отмечаем все чекбоксы, выбираем release и нажимаем Finish - Android Studio скомпилирует подписанное приложение, которое можно опубликовать в Google Play.

Итог

После загрузки файла приложения APK потребуется заполнить множество форм и подготовить множество материалов: описание на разных языках (если необходимо), изображения на разных языках (надписи на изображениях я имею в виду), логотипы, иконки разных размеров, скриншоты со смартфона и планшета.

Наконец отправляем приложение в публикацию. Сотрудники Google Play будут проверять ваше приложение в течении 2 недель, судя по официальным данным. Данное приложение рассматривали в течении 5 суток. Также, стоит учесть, что каждое обновление, также, будут проверять, но на обновления уходит не более 2-3 суток.

Ссылка на GitHub, как обещано. Ссылка на приложение в Google Play.

Подробнее..

Всего лишь меняем модель эмулятора Android устройства

22.01.2021 16:14:24 | Автор: admin

Пролог

Казалось бы, на первый взгляд весьма простая задача. Некоторые читатели могли еще в те бородатые времена лазить по всяким 4пда, рутить свой сенсорный самсунг, менять содержимое файла build.prop и показывать наивным ламерам свой iPhone 15+ Max Pro. Однако, как оказалось, и как оно часто бывает, не все так просто и здесь есть свои подводные камни. Статья призвана помочь простым работягам избежать все кочки да ямы на пути к своей цели!

Дисклеймер

Сразу предупрежу, что люблю писать подобные статьи довольно подробно, не ради объема и многобукав, а ради максимального погружения в проблему и способ ее решения. Обратите внимание, что я работаю на macOS, поэтому все команды в терминале будут ориентированы под данную ОС. Также, следует отметить, что проворачиваювсе это для API 30, то есть для самого последнего на момент написания статьи. Как говорят интернеты, сложности по этой теме начались с API 29.

Зачем это нужно?

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

Ради контекста вкратце расскажу зачем это понадобилось мне. Я получил .apk с багом где-то внутри приложения. Однако пройти дальше первого экрана в этом приложении я не смог. Дело в том, что при запуске, с сервера приходит список разрешенных для запуска устройств и ни мой народный Ксяоми, ни мой эмулятор в этот список не входит. Вот и додумался поменять имя модели устройства на одно из разрешенных. Рутить свой личный телефон не хотелось, поэтому решил шаманить с эмулятором.

Экран не пустивший меня дальшеЭкран не пустивший меня дальше

Достаем build.prop

Как уже говорилось в начале статьи, за имя производителя и модель устройства отвечает системный файл build.prop, который находится в корне устройства в папке system/. Однако при попытке просмотреть его, не говоря уже о редактирование, мы получим отказ в доступе:

Для решения этой проблемы необходимо в терминале обратиться к adb и запросить root права следующей командой: adb root. И вот и первый подводный камень, а именно вывод следующего сообщения: adbd cannot run as root in production builds. Это из-за того что при создании эмулятора мы выбрали вариант с установленными Google сервисами:

Простое решение - создать эмулятор без установленных Google сервисов, после чего повторить команду adb root. После чего в консоле должно появится сообщение: restarting adbd as root что говорит об успешном проведении операции. Естественно если с самого начала у вас был эмулятор без Google сервисов, то скорее всего с adb root и выше описанной проблемой вы не столкнулись.

Отлично, теперь мы видим содержимое файла build.prop:

Редактируем build.prop

Сохраним файл build.prop в любое удобное место для дальнейшего редактирования выделенной красным области на скриншоте выше. Я сохранил прямо на рабочий стол:

Вносим необходимые изменения. Просмотрев логи запросов и ответов предоставленного мне .apk я нашел приходящий с сервера список разрешенных устройств. То есть, для моих целей нужно поменять два значения на PIXEL 3A XL (как вы поняли, здесь вы можете указывать необходимую именно вам модель):

Сохраняем изменения и заливаем файл обратно на эмулятор. Делается это при помощи команды adb push (кстати, скачать файл с эмулятора можно при помощи adb pull если у вас вдруг аллергия на GUI).

Вводим команду в терминал: adb push build.prop system/

И получаем ошибку:

adb: error: failed to copy 'build.prop' to 'system/build.prop': remote couldn't create file: Read-only file system

Вот здесь и начинается самое интересное! По умолчанию эмулятор запускается в режиме чтения системных файлов, без возможности делать записи. Следовательно, что либо поменять без прав на запись у нас не выйдет. Для этого нам необходимо запустить эмулятор в ручном режиме с доступом на запись системных файлов.

Запускаем эмулятор с доступом на перезапись системных файлов

Для этого нужно выполнить следующую команду в терминале (чтобы скорее всего получить еще одну ошибку):

emulator -avd Pixel3XLAPI30 -writable-system -no-snapshot -nocache

итак здесь Pixel3XLAPI30 - это название нашего эмулятора который мы будем запускать в режиме записи, получить это имя можно выполнив команду emulator -list-avds

-writable-system - собственно тот самый флаг и виновник торжества.

-no-snapshot -nocache - просто советую ввести чтобы избавиться от любого возможного мусора, который может помешать нашему плану-капкану.

После у нас либо запуститься эмулятор (несколько секунд запускается, так что если тупит то так и должно быть) либо получаем ошибку следующего типа:

PANIC: Missing emulator engine program for 'x86' CPU.

БЕЗ ПАНИКИ! Не удивительно, что интернет уже решал подобную проблему.

Что бы и нам порешать с этим нужно в файле .bash-profile (или если у вас zsh то в файле .zshenv) находящийся в корне вашего профиля macOS, добавить дополнительные пути. Вот как это выглядит у меня:

export ANDROIDHOME=~/Library/Android/sdk

есть такая переменная ANDROIDHOME и с ее участием редактируем переменную PATH:

export PATH=$ANDROIDHOME/emulator:$ANDROIDHOME/tools:$PATH

Чтобы изменения вступили в силу перезапускаем терминал (или вводим source ~/.bash_profile) (или source ~/.zshenv). Результат можно проверить выполнив команду echo $PATH и убедиться что в переменной PATH появился добавленный нами путь.

Пробуем запустить эмулятор еще раз.

emulator -avd Pixel3XLAPI30 -writable-system -no-snapshot -nocache

Теперь он должен был успешно запустится.

Активируем доступ на перезапись системных файлов

Из описания флага -writable-system:

-writable-system make system & vendor image writable after 'adb remount'

делаем вывод что теперь нам нужно выполнить adb remount. Для этого открываем новое окно терминала и выполняем сначала команду adb root, что бы adb remount сработало.

После adb remount, будет сообщение что эмулятор нужно перезапустить. Сделать это можно командой adb reboot. Но и здесь не все так просто. Очередной подводный камень об который мы разбили еще один ноготь на пальцах ног. Если сделатьadb reboot то все просто напросто зависает НАВСЕГДА. Настолько навсегда, что придется удалять эмулятор и создавать его заново. Интернет и с этим столкнулся и даже баг создали на гуглов. И благо нашлось решение. Чтобы эмулятор не зависал нужно добавить пару команд до adb remount.

Итак по порядку:

  1. Делаем adb root

  2. Теперь делаем adb shell avbctl disable-verification

  3. Если вы вдруг остались в shell то введите exit

  4. Перезагружаем эмуляторadb reboot и ждем

  5. Снова делаем adb root

  6. И вот теперь можно делать adb remount

Ура! Теперь мы можем записывать файлы в системную папку нашего эмулятора. Можем пушнуть наш отредактированный build.prop файл: adb push build.prop system/. Сделаем adb reboot и убеждаемся что ничего не поменялось Имя модели не изменилось...

Редактируем правильный build.prop

Вернемся к началу и заметим, что значения ro.product.product.name и ro.product.product.model не соответствует тому, что отображается в настройках устройства. Изучив структуру системных папок я заметил, что существует несколько файлов build.prop, которые располагаются в папках: system, system_ext, vendor и product. Эмпирическим методом я скачивал, редактировал и пушил обратно каждый из этих файлов. В конце концов ключевым оказался файл в папке product. Отредактировав его я наконец-то смог изменить название модели эмулятора устройства!

Подводим итоги

Наконец-то я смогу запустить приложение и воспроизвести баг. Подумал я

Теперь я уперся в то, что запускаю приложение якобы с рутованого девайса (ну да есть такой грешок). И дело даже не в команде adb root, ведь команда adb unroot не помогла. Что ж, опускать руки уже поздно, придется что-то придумать.

О том, как я обходил проверку на рутованность устройства я расскажу в следующей своей статье. Немного реверс инжиниринга и даже такая популярная библиотека как RootBeer не проблема.

Данной статьей я стремился собрать как можно больше проблем по этому вопросу и изложить все в форме step-by-step. Спасибо за ваше внимание и очень надеюсь, что статья оказалась полезной!

Подробнее..

Как обойти проверку на Рутинг устройства обхитрив библиотеку RootBeer?

25.01.2021 14:08:49 | Автор: admin

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

Дисклеймер

Сразу предупрежу, что люблю писать подобные статьи довольно подробно, не ради объема и многобукав, а ради максимального погружения в проблему и способ ее решения. Обратите внимание, что я работаю на macOS, поэтому все команды в терминале будут ориентированы под данную ОС.

Как до этого дошло?

Мне выслали .apk файл приложения. Где-то внутри приложения есть баг, который мне нужно воспроизвести. Но пройти вглубь приложения у меня не получается. Мое устройство не проходит проверку на рутованность.

Экран, который меня не пропускаетЭкран, который меня не пропускает

Так как я уже потратил немало усилий, чтобы все же запустить это приложение (о каких именно усилиях идет речь можно узнать в прошлой статье), то и здесь я решил не сдаваться и попробовать обойти проверку на рутованность устройства.

Что будем делать?

Первое, что пришло мне в голову, так это попробовать выполнить декомпиляцию и поправить кое-где код, а потом собрать все обратно. Основное сомнение по поводу такого подхода вызывало то что, у меня не нативное android-приложение, а flutter-приложение. То есть как такового основного кода приложения в виде байт-кода в .apk найти не получится. Или получится? И нужен ли мне код самого приложения?

Небольшое отступление о том, как во флаттере запускается нативный код, то есть родной код для Android или iOS платформы. Например, перед flutter-разработчиком стоит задача: не давать возможность работать с приложением на рутованном устройстве. Аналогом андроид root-устройства является jailbreak для iOS. Для проверки на рутованность или jailbreak необходимо воспользоваться нативными средствами обеих платформ. Для этого разработчик пишет плагин, который вызывает соответствующие нативные методы в зависимости от платформы на которой запущено приложение.

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

Я решил, что буду менять байт-код во втором месте. Честно говоря, сначала я работал с первым местом, то есть с плагином. Все потому, что кода там намного меньше и запутаться было сложнее. Всего лишь метод, который вызывает другой метод - это пара строк кода. Но в таком случае статья была бы еще сильнее завязана под конкретный .apk, и не было б такой абстрактности. Поэтому будем работать напрямую с кодом библиотеки.

Смотрим байт-код .apk

Итак, посмотрим на внутренности .apk. Для этого можно воспользоваться инструментом, предоставляемый Android Studio. В меню выбираем Build -> Analyze APK В окне выбора файла находим интересующий нас .apk.

Весь скомпилированный байт-код хранится в файле classes.dex. Выберем его и взглянем на его содержимое. В первую очередь нас интересуют не файлы java или androidx, а именно файлы приложения. Поэтому выбираем первый package в структуре файлов android проекта, в моем случае это папка com. Из любопытства к содержимому каждой из папок я затригерился на слово rootbeer. Погуглив в гугле оказалось, что это весьма популярная библиотека для проверки устройства на предмет рутованности.

Исследовав документацию и исходники библиотеки стало ясно о том, как пользоваться библиотекой и где располагается ключевой метод isRooted().

Даже не смотря на то, что большинство классов приложения были обфусцированы, по прежнему получается сориентироваться и найти нужный файл. Не хочу сказать, что обфускация бесполезна. Ведь будь необходимый мне файл где-то в глубине приложения, то я б скорее всего его не нашел.

Судя по исходникам библиотеки, нужный мне файл должен находится рядом с классом RootBeerNative, и как мы видим тут таких, два a и b. Эмпирическим методом я понял, что это файл b. Правый клик по файлу b и в выпадающем меню выбираем Show Bytecode.

К сожалению, имена всех методов обфусцированы. Снова обращаясь к исходному файлу на github становится ясно, что метод isRooted() находится на строке 42. Побегав по файлу с байт-кодом мне удалось найти этот метод. Только теперь он называется a() и почему-то начинает работу с .line 43. Вот этот метод я и буду редактировать. Но не так быстро, К сожалению здесь только Read-only доступ.

Редактируем содержимое classes.dex

Для этого придется декомпилировать весь .apk при помощи популярного инструмента apktool. Работать с ним довольно просто. После его установки нам понадобятся всего 2 команды:

apktool d appname.apk, которая декомпилирует указанный .apk и apktool b directory_with_app, которая соберет .apk обратно (в указанной directory_with_app должен располагаться файл apktool.yml).

Итак, выполняю декомпиляцию при помощи команды в терминале: apktool d app.apk

После выполнения команды появляется папка с файлами приложения. Нас интересует папка smali. Smali - это язык которым описан байт-код (вот классная статья по основам smali). Именно эта папка и есть содержимое файла classes.dex. К только что выполненной команде можно добавить параметр --skip-sources или просто -s тогда вместо папки smali мы увидим тот самый файл classes.dex.

Далее, в папке smali находим интересующий и уже известный нам файл b.smali:

Открываем его любым текстовым редактором и переходим к месту, где располагается метод isRooted():

Содержимое файла b.smali
.method public a()Z    .locals 1    .line 43    invoke-virtual {p0}, Lcom/scottyab/rootbeer/b;->c()Z    move-result v0    if-nez v0, :cond_1    invoke-virtual {p0}, Lcom/scottyab/rootbeer/b;->d()Z    move-result v0    if-nez v0, :cond_1    const-string v0, "su"    invoke-virtual {p0, v0}, Lcom/scottyab/rootbeer/b;->a(Ljava/lang/String;)Z    move-result v0    if-nez v0, :cond_1    const-string v0, "busybox"    .line 44    invoke-virtual {p0, v0}, Lcom/scottyab/rootbeer/b;->a(Ljava/lang/String;)Z    move-result v0    if-nez v0, :cond_1    invoke-virtual {p0}, Lcom/scottyab/rootbeer/b;->f()Z    move-result v0    if-nez v0, :cond_1    invoke-virtual {p0}, Lcom/scottyab/rootbeer/b;->g()Z    move-result v0    if-nez v0, :cond_1    .line 45    invoke-virtual {p0}, Lcom/scottyab/rootbeer/b;->b()Z    move-result v0    if-nez v0, :cond_1    invoke-virtual {p0}, Lcom/scottyab/rootbeer/b;->h()Z    move-result v0    if-nez v0, :cond_1    invoke-virtual {p0}, Lcom/scottyab/rootbeer/b;->j()Z    move-result v0    if-nez v0, :cond_1    invoke-virtual {p0}, Lcom/scottyab/rootbeer/b;->e()Z    move-result v0    if-eqz v0, :cond_0    goto :goto_0    :cond_0    const/4 v0, 0x0    goto :goto_1    :cond_1    :goto_0    const/4 v0, 0x1    :goto_1    return v0.end method

Можем сравнить его с неповторимым оригиналом:

Очень похожи. Итак, нас интересует return, то есть самый конец метод. Видим, что метод возвращает некоторое значение константы v0, значение которой определяется на основе выполнивших в методе условий. Нам это не подходит. Нам всегда нужно возвращать false или же как это будет в smali 0x0. Для этого добавим свою констант v1 со значением 0x0. Сделаем это прямо над return:

:goto_1const/4 v1, 0x0return v1

И конечно же заменим в return v0 на v1. Поднимемся в самое начало метода и изменим значение .locals с 1 на 2 потому, что так надо. Сохраняем изменения в файле и закрываем редактор.

Собираем .apk обратно

Для этого воспользуемся командой apktool b app, где app - папка с приложением, в котором мы редактировали smali файл. После завершения команды, в папке app появиться директория dist. Здесь и расположен наш заново собранный .apk. Однако при попытке установить его мы получим следующую ошибку:

The APK failed to install.Error: INSTALLPARSEFAILEDNOCERTIFICATES: Failed collecting certificates for /data/app/vmdl164530405.tmp/base.apk: Failed to collect certificates from /data/app/vmdl164530405.tmp/base.apk: Attempt to get length of null array

Это из-за того, что после пересборки нужно еще и переподписать приложение. Для этого нам понадобиться любой keystore файл. Или можно создать новый при помощи команды:

keytool -genkey -v -keystore my-release-key.keystore -alias alias_name -keyalg RSA -keysize 2048 -validity 10000

Подписать ключом приложение можно при помощи одного из двух инструментов: apksigner или jarsigner.

Я выбрал jarsigner используя следующую команду:

jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1 -keystore keystore app.apk cp_key

В этой команде мы указываем путь к keystore, путь к .apk, который хотим подписать и имя ключа (alias) из указанного keystore. После, вводим пароль от keystore и приложение успешно подписано.

Однако и это еще не все, теперь при попытке установить .apk мы будем получать другую ошибку:

The APK failed to install.Error: INSTALL_FAILED_INVALID_APK: Failed to extract native libraries, res=-2

Это потому, что после подписи приложения нужно выполнить оптимизацию .apk файла при помощи инструмента zipalign.

Для выполнения оптимизации нужно ввести следующую команду: ~/Library/Android/sdk/build-tools/30.0.3/zipalign -v -f -p 4 app.apk rdy.apk

30.0.3 - я выбрал самую последнюю версию на момент написания статьи. После завершения команды на выходе получаем файл rdy.apk, который можно успешно установить! Проверим, получилось ли обойти проверку на рутованность устройства:

И да, это успех!

Заключение

Вызов принят - вот, что я подумал получив этот .apk. И считаю это маленькой победой. Данной статьей я стремился описать не просто инструкцию о том, как расковырять приложение или свои способности по типу "смотрите как могу". Этой статьей я хочу сказать (и напомнить тем, кто стал забывать) что большинство проблем решаемы, нужно только копать, копать и еще раз копать, не опускать руки и результат обязательно будет удовлетворительным. Даже если, в конце концов, баг я так и не обнаружил, я очень рад что расширил свой кругозор и в дальнейшем смогу взяться за более сложные задачи! Спасибо за внимание, обязательно буду писать еще про подобные танцы с бубном!

Подробнее..

Превращаем EditText в SearchEditText

12.09.2020 22:20:08 | Автор: admin
image

Пробовали ли Вы когда-нибудь настроить внешний вид или поведение стандартного компонента SearchView? Полагаю, да. В этом случае, я думаю что вы согласитесь, что далеко не все его настройки являются достаточно гибкими, для того, чтобы удовлетворить всем бизнес-требованиям отдельно взятой задачи. Одним из способов решения этой проблемы является написание собственного кастомного SearchView, чем мы сегодня и займемся. Поехали!

Примечание: создаваемое view (далее SearchEditText), не будет обладать всеми свойствами стандартного SearchView. В случае необходимости, вы можете без труда добавить дополнительные опции под конкретные нужды.

План действий


Есть несколько вещей, которые нам нужно сделать, для превращения EditText в SearchEditText. Если кратко, то нам нужно:

  • Унаследовать SearchEditText от AppCompatEditText
  • Добавить иконку Поиск в левом (или правом) углу SearchEditText, при нажатии на которую введённый поисковый запрос будет передаваться зарегистрированному слушателю
  • Добавить иконку Очистка в правом (или левом) углу SearchEditText, при нажатии на которую введённый текст в поисковой строке будет очищаться
  • Установить в параметре imeOptions SearchEditText-а значение IME_ACTION_SEARCH, для того, чтобы при появлении клавиатуры кнопка ввода текста выполняла роль кнопки Поиск

SearchEditText во всей красе!


import android.content.Contextimport android.util.AttributeSetimport android.view.MotionEventimport android.view.View.OnTouchListenerimport android.view.inputmethod.EditorInfoimport androidx.appcompat.widget.AppCompatEditTextimport androidx.core.widget.doAfterTextChangedclass SearchEditText@JvmOverloads constructor(    context: Context,    attributeSet: AttributeSet? = null,    defStyle: Int = androidx.appcompat.R.attr.editTextStyle) : AppCompatEditText(context, attributeSet, defStyle) {    init {        setLeftDrawable(android.R.drawable.ic_menu_search)        setTextChangeListener()        setOnEditorActionListener()        setDrawablesListener()        imeOptions = EditorInfo.IME_ACTION_SEARCH    }    companion object {        private const val DRAWABLE_LEFT_INDEX = 0        private const val DRAWABLE_RIGHT_INDEX = 2    }    private var queryTextListener: QueryTextListener? = null    private fun setTextChangeListener() {        doAfterTextChanged {            if (it.isNullOrBlank()) {                setRightDrawable(0)            } else {                setRightDrawable(android.R.drawable.ic_menu_close_clear_cancel)            }            queryTextListener?.onQueryTextChange(it.toString())        }    }        private fun setOnEditorActionListener() {        setOnEditorActionListener { _, actionId, _ ->            if (actionId == EditorInfo.IME_ACTION_SEARCH) {                queryTextListener?.onQueryTextSubmit(text.toString())                true            } else {                false            }        }    }        private fun setDrawablesListener() {        setOnTouchListener(OnTouchListener { view, event ->            view.performClick()            if (event.action == MotionEvent.ACTION_UP) {                when {                    rightDrawableClicked(event) -> {                        setText("")                        return@OnTouchListener true                    }                    leftDrawableClicked(event) -> {                        queryTextListener?.onQueryTextSubmit(text.toString())                        return@OnTouchListener true                    }                    else -> {                        return@OnTouchListener false                    }                }            }            false        })    }    private fun rightDrawableClicked(event: MotionEvent): Boolean {        val rightDrawable = compoundDrawables[DRAWABLE_RIGHT_INDEX]        return if (rightDrawable == null) {            false        } else {            val startOfDrawable = width - rightDrawable.bounds.width() - paddingRight            val endOfDrawable = startOfDrawable + rightDrawable.bounds.width()            startOfDrawable <= event.x && event.x <= endOfDrawable        }    }    private fun leftDrawableClicked(event: MotionEvent): Boolean {        val leftDrawable = compoundDrawables[DRAWABLE_LEFT_INDEX]        return if (leftDrawable == null) {            false        } else {            val startOfDrawable = paddingLeft            val endOfDrawable = startOfDrawable + leftDrawable.bounds.width()            startOfDrawable <= event.x && event.x <= endOfDrawable        }    }    fun setQueryTextChangeListener(queryTextListener: QueryTextListener) {        this.queryTextListener = queryTextListener    }    interface QueryTextListener {        fun onQueryTextSubmit(query: String?)        fun onQueryTextChange(newText: String?)    }}

В приведенном выше коде были использованы две extension-функции для установки правого и левого изображения EditText-а. Эти две функции выглядят следующим образом:

import android.widget.TextViewimport androidx.annotation.DrawableResimport androidx.core.content.ContextCompatprivate const val DRAWABLE_LEFT_INDEX = 0private const val DRAWABLE_TOP_INDEX = 1private const val DRAWABLE_RIGHT_INDEX = 2private const val DRAWABLE_BOTTOM_INDEX = 3fun TextView.setLeftDrawable(@DrawableRes drawableResId: Int) {    val leftDrawable = if (drawableResId != 0) {        ContextCompat.getDrawable(context, drawableResId)    } else {        null    }    val topDrawable = compoundDrawables[DRAWABLE_TOP_INDEX]    val rightDrawable = compoundDrawables[DRAWABLE_RIGHT_INDEX]    val bottomDrawable = compoundDrawables[DRAWABLE_BOTTOM_INDEX]    setCompoundDrawablesWithIntrinsicBounds(        leftDrawable,        topDrawable,        rightDrawable,        bottomDrawable    )}fun TextView.setRightDrawable(@DrawableRes drawableResId: Int) {    val leftDrawable = compoundDrawables[DRAWABLE_LEFT_INDEX]    val topDrawable = compoundDrawables[DRAWABLE_TOP_INDEX]    val rightDrawable = if (drawableResId != 0) {        ContextCompat.getDrawable(context, drawableResId)    } else {        null    }    val bottomDrawable = compoundDrawables[DRAWABLE_BOTTOM_INDEX]    setCompoundDrawablesWithIntrinsicBounds(        leftDrawable,        topDrawable,        rightDrawable,        bottomDrawable    )}

Наследование от AppCompatEditText


class SearchEditText@JvmOverloads constructor(    context: Context,    attributeSet: AttributeSet? = null,    defStyle: Int = androidx.appcompat.R.attr.editTextStyle) : AppCompatEditText(context, attributeSet, defStyle)

Как видите, из написанного конструктора мы передаём все необходимые параметры в конструктор AppCompatEditText. Важным моментом тут является то, что значением defStyle по-умолчанию является android.appcompat.R.attr.editTextStyle. Наследуясь от LinearLayout, FrameLayout и некоторых других view, мы, как правило, используем 0 в качестве значения по-умолчанию для defStyle. Однако в нашем случае это не подходит, иначе наш SearchEditText будет вести себя как TextView, а не как EditText.

Обработка изменения текста


Следующее, что нам нужно сделать, это научиться реагировать на события изменения текста нашего SearchEditText. Нам это необходимо по двум причинам:

  • отображение или скрытие иконки очистки в зависимости от того, введён ли текст
  • оповещение слушателя об изменении текста в SearchEditText

Посмотрим на код слушателя:

private fun setTextChangeListener() {    doAfterTextChanged {        if (it.isNullOrBlank()) {            setRightDrawable(0)        } else {            setRightDrawable(android.R.drawable.ic_menu_close_clear_cancel)        }        queryTextListener?.onQueryTextChange(it.toString())    }}

Для обработки событий изменения текста использовалась extension-функция doAfterTextChanged из androidx.core:core-ktx.

Обработка нажатия кнопки ввода на клавиатуре


Когда пользователь нажимает клавишу ввода на клавиатуре, происходит проверка на то, является ли это действие IME_ACTION_SEARCH. Если это так, то мы сообщаем слушателю об этом действии и передаем ему текст из SearchEditText. Посмотрим как это происходит.

private fun setOnEditorActionListener() {    setOnEditorActionListener { _, actionId, _ ->        if (actionId == EditorInfo.IME_ACTION_SEARCH) {            queryTextListener?.onQueryTextSubmit(text.toString())            true        } else {            false        }    }}

Обработка нажатий на иконки


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

Для решения этой проблемы был зарегистрирован OnTouchListener в SearchEditText. При касании, с помощью функций leftDrawableClicked и rightDrawableClicked мы теперь можем обрабатывать клик по иконкам. Взглянем на код:

private fun setDrawablesListener() {    setOnTouchListener(OnTouchListener { view, event ->        view.performClick()        if (event.action == MotionEvent.ACTION_UP) {            when {                rightDrawableClicked(event) -> {                    setText("")                    return@OnTouchListener true                }                leftDrawableClicked(event) -> {                    queryTextListener?.onQueryTextSubmit(text.toString())                    return@OnTouchListener true                }                else -> {                    return@OnTouchListener false                }            }        }        false    })}private fun rightDrawableClicked(event: MotionEvent): Boolean {    val rightDrawable = compoundDrawables[DRAWABLE_RIGHT_INDEX]    return if (rightDrawable == null) {        false    } else {        val startOfDrawable = width - rightDrawable.bounds.width() - paddingRight        val endOfDrawable = startOfDrawable + rightDrawable.bounds.width()        startOfDrawable <= event.x && event.x <= endOfDrawable    }}private fun leftDrawableClicked(event: MotionEvent): Boolean {    val leftDrawable = compoundDrawables[DRAWABLE_LEFT_INDEX]    return if (leftDrawable == null) {        false    } else {        val startOfDrawable = paddingLeft        val endOfDrawable = startOfDrawable + leftDrawable.bounds.width()        startOfDrawable <= event.x && event.x <= endOfDrawable    }}

В функциях leftDrawableClicked и RightDrawableClicked нет ничего сложного. Возьмём, к примеру, первую из них. Для левой иконки мы сначала рассчитываем startOfDrawable и endOfDrawable, а затем проверяем, находится ли x-координата точки касания в диапазоне [startofDrawable, endOfDrawable]. Если да, то это означает, что левая иконка была нажата. Функция rightDrawableClicked работает аналогичным образом.

В зависимости от того, нажата ли левая или правая иконка, мы осуществляем те или иные действия. При нажатии на левую иконку (значок поиска) мы сообщаем об этом слушателю, вызывая его функцию onQueryTextSubmit. При нажатии на правую очищаем текст SearchEditText.

Вывод


В этой статье мы рассмотрели вариант превращения стандартного EditText в более продвинутый SearchEditText. Как уже упоминалось ранее, готовое решение не поддерживает все параметры, предоставляемые SearchView, однако вы в любой момент можете его усовершенствовать, добавив дополнительные опции на свое усмотрение. Дерзайте!

P.S:

Доступ к исходному коду SearchEditText вы можете получить из этого репозитория GitHub.
Подробнее..

Как использовать облачную ферму устройств Huawei для тестирования и отладки в Anrdoid Studio

11.05.2021 14:17:08 | Автор: admin

Как ни странно, мало кто знает о том, что у Huawei есть ферма устройств в облаке, которую можно использовать для отладки и тестирования. И речь идет не об отладке через веб-интерфейс, что является более-менее известной фичёй консоли разработчика Huawei. Мы поговорим об отладке непосредственно из студии, с возможностью пользоваться ADB.

Для этого нам понадобится аккаунт разработчика Huawei, который можно зарегистрировать тут.

Следующим шагом открываем студию и устанавливаем плагин HMS Toolkit из магазин плагинов в самой студии (File -> Settings -> Plugins).

В верхнем меню появится пункт HMS, в котором будет всё, относящееся к Huawei Mobile Services, но нас сейчас интересует именно Cloud Debugging.

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

Лично я предпочитаю логиниться по QR-коду, но опять же, сейчас это неважно. После успешного логина нам надо будет подтвердить, что мы выполняем логин для HMS Toolkit.

Далее нам будет показана страница с результатом, и мы переключимся в студию где откроется страница с выбором устройства.

Выбираем нужное нам устройство, если оно не занято, подключаемся. Перед каждым использованием устройство сбрасывается, так что нам необходимо будет пройти на нём быстрый онбординг и можно использовать. Можно запускать своё приложение, можно подключаться к процессу и отлаживаться.

Можно выполнять все команды ADB, получать с устройства скриншоты и так далее.

Мне эта ферма помогла отладить своё приложение на разных моделях телефонов от Huawei. Когда-то в ней были и телефоны Honor, но сейчас когда бизнес Honor продан этих телефонов в ферме уже нет.

Если кому-то интересно, устройства в ферме все реальные, можно включить камеру с подсветкой и увидеть даже соседние девайсы.

В качестве заключения хочу добавить. Конечно, полноценно использовать облачное устройство не получится. Более того, в чём-то его возможности даже уступают эмулятору, так как будет затруднительно на устройстве тестировать кейсы с поворотами устройства, сложными жестами и другими действиями, требующими доступа непосредственно к устройству. Но для беглых смоук-тестов и для отладки на живом устройстве вполне подходит.

Подробнее..

Беспроводная отладка Android 11

06.01.2021 14:15:34 | Автор: admin
Режим дебага по WiFi теперь доступен, начиная с версии ОС Android 11. Давайте разберемся, как подключить устройство по Wi-Fi и смотреть логи в Logcat.




Нам необходимо убедиться, что у нас имеется все необходимое для соединения, а именно:

  1. установлен компонент Android SDK Platform-Tools версии не ниже 30.0.0 (April 2020), но естественно необходимо поставить последнюю версию, в которой разработчики пофиксили существующие на данный момент баги;
  2. включен режим разработчика на вашем устройстве;
  3. на устройстве версия ОС Android 11.

Переходим в режим разработчика на нашем устройстве и активируем Отладку по Wi-Fi.



Далее необходимо выбрать раздел Подключить устройство с помощью кода подключения.



В боттомшите отобразится сам код подключения к устройству и IP-адрес и порт.



Откроем Android Studio, перейдем во вкладку Terminal, далее введем и выполним команду adb pair ipaddr:port где ipaddr и port данные из боттомшита Подключение к устройству. Следующим шагом вводим код подключения и получим push на устройство об успешном подключении.



Перейдем на вкладку Logcat и все, что нам осталось выбрать наше устройстве из списка подключенных устройств, в конкретном примере это Samsung SM-N985F.



Резюме


Следите за обновлениями в новых версиях и не ленитесь читать официальную документацию и узнавать о новых фишках.
Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru