Introduction into Android Development for the stupid.

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

Методичка будет структурирована примерно следующим образом:

  1. Введение: почему я решил написать ещё одну методичку на тему, на которую уже всё написано.
  2. Tooling для Android, инструменты и образ мышления.
    1. Android Studio.
    2. Эмулятор и страдания с ним.
    3. ADB.
    4. Emacs и jdtls.
  3. Gradle и как её использовать с минимумом страданий. Разбор дефолтного проекта из Android Studio.
    1. Gradle.
    2. Proguard.
    3. Репозитории, зависимости, версии.
    4. Dex, multidex, Smali, исходники.
  4. Структура программ для Андроид и IPC.
    1. Внутренняя структура программ.
    2. Activities, IPC, Intents, Broadcast Receivers.
    3. Manifest, R, aapt2 и прочее XML-говно.
    4. Binder.
  5. Тестирование.
  6. Android.mk и встройка в Андроид-программы нормального кода.
  7. Асинхронное говно и методы борьбы ним. Структура программ 2.
    1. Bundle и Preferences.
    2. Персистентные нотификации.
    3. Трэды и алярмы.
    4. Провайдеры данных и говённая Android VFS.
    5. Jetpack Compose, Androidx и Appcompat.
    6. Темы https://stackoverflow.com/questions/21814825/you-need-to-use-a-theme-appcompat-theme-or-descendant-with-this-activity
  8. Тоталитарное говно и способы справляться с ним.
    1. Firebase.
    2. Play Store.
  9. Сторонние, но полезные, инструменты.
    1. Котлин (морские ворота Петербурга)
    2. Dagger
    3. Mortar
  10. Уже реализованные функции.
    1. Emacs.
    2. OpenKeyChain.
    3. Termux.

References:

  1. https://www.geeksforgeeks.org/introduction-to-android-development/
  2. https://www.geeksforgeeks.org/best-way-to-become-android-developer-a-complete-roadmap/

1. TODO Body

1.1. TODO Введение

Итак, зачем нужна эта методичка? Ответ – потому, что штатная документация Google написана очень плохо, и ей невозможно пользоваться. Проблем с ней базово две: (1) она написана таким отвратительным бюрократическим волапюком, что понять в нём решительно ничего не возможно, (2) она сознательно врёт, с целью ввести пользователя в заблуждение и обманом заставить его делать что-то во вред себе, что принесёт прибыль компании, (3) примеры в документации переусложнённые.

Пример (1):

Вот все мы знаем, что в программах бывают кнопочки, нажимая который можно триггерить какие-то действия. Мы также знаем, что в ООП-языках (а Java – это ООП язык), обычно виджеты ассоциированы с объектами каких-то классов, например конкретная кнопка (виджет на экране) ассоциирована с классом Button. Бывает, конечно, и не ООП-дизайн, но в Java обычно всё ООП.

Теперь смотрим, какие кнопочки бывают в Android:

  1. android.widget.Button :: https://developer.android.com/reference/android/widget/Button
  2. androidx.appcompat.widget.AppCompatButton extends android.widget.Button :: https://developer.android.com/reference/androidx/appcompat/widget/AppCompatButton

Хотим понять, чем они отличаются, читаем: "A Button which supports compatible features on older versions of the platform". Думаем, "ага, ну наверное, Button это что-то свеженькое и новое, а AppCompatButton нужна для поддержки каких-то старых устройств". Так? А вот хрен два. Button существует в Android с незапамятных времён, с самой первой версии в ней были какие-то кнопочки, а AppCompatButton – это виджет "нового поколения", из пакета androidx. Обычно Compatibility означает, что мы можем взять старую программу, запустить на новой системе, и она автоматически получит хотя бы какой-то функционал новой системы, ну, до той степени, до которой он не конфликтует с логикой программы. Но у Google всегда всё сделано не по-человечески, поэтому не надейтесь на логику.

Что же "на самом деле" имеется в виду? "На самом деле" имеется в виду, что ВООБЩЕ НЕТ никакого нормально определения того, что такое android.widget.Button. Определение класса меняется по мановению левой ноги инженера в Google, установленной версии Sdk, выбранной версии Sdk, версии Андроида на целевом устройстве, компилятора Java и погоды на Марсе. То есть, по сути классс android.widget.Button вообще бесполезен для обыденного использования. Класс androidx.appcompat.widget.AppCompatButton – это то, что обеспечивает "до какой-то степени" совместимость между версиями и предоставляет хоть в какой-то мере стабильный API.

Пример (2):

Что такое Material Design, и нахер он нужен? Я посмотрел в Википедии https://en.wikipedia.org/wiki/Material_Design и ни черта не понял. Похоже не очередные бессмысленные финтифлюшки для визуалов. В этой методичке, видимо, придётся рассмотреть, что они предлагают, но большую часть отбросить, потому что она бессмысленная.

Смысл тут в том, что Google хочет заставить программистов не просто писать программы "по стандарту", а хочет, чтобы программы соответствовали их эстетическому чувству.

1.3. TODO Java и Gradle

  1. TODO Java

    Люди говорят, что Android написан на Java, но что основной язык разработки для Android – это Kotlin.

    И то, и другое, мягко говоря, сомнительно.

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

    В итоге разбираться со всем этим довольно сложно, а может и не стоит.

    Надо иметь в виду, что в итоговом пайплайне у вас будут следующие реализации Java:

    1. JRE (или JDK), в которой запускается AndroidStudio
    2. JRE (или JDK), в которой запускается Gradle
    3. JDK, которым компилируется код программы
    4. JRE (Dalvik/Art), который работает на целевом устройстве
    5. JDK для minSdk
    6. JDK для targetSdk
    7. JDK для compileSdk

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

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

    android {
      // ...
      compileOptions {
    	sourceCompatibility JavaVersion.VERSION_17
    	targetCompatibility JavaVersion.VERSION_17
    	coreLibraryDesugaringEnabled true
        }
      // ...
    }
    dependencies {
      // ...
      coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.4'
      // ...
    }
    

    Что это ещё за desugaring такой?

    Java 8+ APIs available through desugaring

    bookmark_border Android Studio now includes support for using a number of Java 8+ APIs without requiring a minimum API level for your app. Through a process called API desugaring, the DEX compiler (D8) allows you to include more standard language APIs in apps that support older versions of Android.

    Вы что-нибудь поняли? Я ни хрена не понял. Кажется, это какой-то непонятный инструмент, чтобы компилировать код на современной Java 17/21 для древних JVM 8,9. Но я точно не уверен, потому что документация полное говно. В любом случае, включаем, и если ломается, выключаем.

  2. TODO Gradle

    Gradle – это андроидный аналог Make или CMake. Он очень сложный, и, по возможности, его стоит избегать, как делают, например, разработчики Emacs for Android. Собственно, сборка Android как системы использует не Gradle, а систему Soong, а ранее использовала систему Make. Gradle не является даже основной сборочной системой для Java, для которой обычно используются Ant или Maven. Тем не менее, большинство проектов используют именно Gradle, то есть, поскольку наша задача "побыстрому" запилить в нужный нам проект нужную функцию, избежать Gradle не удастся.

  3. TODO Dependencies

    Describe each clause.

    dependencies {
        implementation libs.appcompat
        implementation 'androidx.appcompat:appcompat:1.6.0'
        implementation(platform("org.jetbrains.kotlin:kotlin-bom:1.8.0"))
        // This does not work:
        // implementation "org.jetbrains.kotlin:kotlin-bom:1.8.0"
        testImplementation libs.junit
        androidTestImplementation libs.ext.junit
    }
    
    

1.4. TODO Структура программ для Андроид и IPC.

1.4.1. TODO AndroidManifest.xml

Вы, наверное, слышали, что программы для Андроида пишутся на Java. Наиболее продвинутые люди слышали что-то про Kotlin. Но на самом деле программы для Андроида пишутся на XML.

В файле AndroidManifest.xml нужно описать "структурные элементы" программы, которые будут затем определять то, как она взаимодействует с другими программами. Но не надо думать, что в AndroidManifest.xml нужно перечислить список путей, по которым программа может создавать named pipe, или список файлов, к которым разрешён доступ по аналогии с AppArmor или SELinux.

AndroidManifest.xml намного сложнее.

https://developer.android.com/guide/topics/manifest/manifest-intro

Самое забавное – это то, что он невероятно хуёвый. Например, он не поддерживает комментарии внутри тэгов.

Вот так можно:

<application
    android:allowBackup="true"
    android:something="meaningless"
    tools:targetApi="31">
    <activity
        bla-bla
</activity>
</application>

А вот так нет:

<application
    android:allowBackup="true"
    <!-- android:something="meaningless" -->
    tools:targetApi="31">
    <activity
        bla-bla
</activity>
</application>

Действительно, верно, что валидный xml не допускает комментарии внутри тэгов, но уж в Android такую чушь могли бы и исправить.

1.4.2. TODO Объект R

R означает "ресурсы", и генерируется из каталога res, процессором xml. Это в каком-то смысле похоже на Qt ресурсы, или даже на cmrc (https://github.com/vector-of-bool/cmrc).

1.4.3. TODO Activity

  1. TODO Что такое Activity и как их писать.

    Activity – это базовый строительный блок Андроид-программ, в каком-то смысле "точка входа", функция main().

    Активити, вроде бы, должна обязательно занимать весь экран, или кусок разделённого экрана в более свежих Андроидах.

    Активити надо задекларировать в AndroidManifest.xml, и создать соответствующий ей класс в Java.

    <manifest ... >
      <application ... >
          <activity android:name=".ExampleActivity" />
          ...
      </application ... >
      ...
    </manifest >
    

    А одной программе может быть много активити.

    У Активити есть lifecycle, то есть, какие её методы вызываются в какой момент.

    Вот есть официальная диаграмма lifecycle:

    https://developer.android.com/reference/android/app/Activity#activity-lifecycle

    И есть неофициальная:

    https://github.com/xxv/android-lifecycle/

  2. TODO Activity и GUI

    Поскольку андроидные люди большей частью пишут GUI, то львиная доля документации посвящена GUI.

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

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

    Грубо говоря, GUI для андроида следует в общем тренде развития GUI в мире: есть программная часть и есть дизайнерская часть, которые более-менее совместимы.

    Дизайнерская часть рисуется графическим редактором, генерирующим xml. XML можно также писать вручную. Он хранится в каталоге res, и получать указатели на его элементы можно с помощью метода Activity.findViewById(R.id.bla).

    Например, у нас есть xml вида:

    <?xml version="1.0" encoding="utf-8"?>
    <androidx.constraintlayout.widget.ConstraintLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:id="@+id/main"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">
        <SurfaceView
            android:id="@+id/first_screen_view"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:layout_marginBottom="32dp"
            app:layout_constraintBottom_toTopOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
    </androidx.constraintlayout.widget.ConstraintLayout>
    

    .MainActivity – это активити, на котором есть ConstraintLayout с id main, внутри которого есть единственный холст SurfaceView с id first_screen_view.

    Нотация у Google как всегда отвратительная. Во-первых, все виджеты унаследованы от View. Почему виджет называется View – за гранью моего понимания, ведь они не являются отображением никаких данных.

    ConstraintLayout – это тоже View, то есть, виджет, по плану Google предназначенный, чтобы содержать другие виджеты. Зачем так делать, совершенно непонятно. Логично было бы сделать какой-нибудь Container с мембером типа LayoutAlgorithm, и он бы все внутренние виджеты разложил так, как умеет, и заменой его можно было бы адаптироваться под разные дизайны и размеры экранов. Но, как всегда, Андроид – полное говно.

    1. Binding

      Ну, конечно, получать указатели на виджеты с помощью findViewById глупо и неудобно, поэтому Google таки придумал механизм binding, пусть и не сразу.

      https://developer.android.com/topic/libraries/data-binding

      По-моему, в С++ такое было ещё в Boland C++ VCL?

      Binding включается в build.gradle:

      dataBinding {
          enabled true
      }
      

      А в java коде пишется:

      import my.package.name.ActivityNameBinding;
      import androidx.databinding.DataBindingUtil;
      
      public class NameActiviy extends AppCompatActivity {
        private ActivityNameBinding binding;
        public void onCreate () {
          this.binding = DataBindingUtil.setContentView(this, R.layout.activity_name);
      
        }
      }
      

      И тогда можно писать проще, без findViewById.

      this.binding.first_screen_view
      .setOnClickListener((final View v) -> { Log.i("bla", "bla");});
      
  3. TODO IPC между разными Activity

    Активити могут получать на вход аргументы, примерно как функция main. Для этого надо в том же xml задекларировать эти аргументы (они называются Intent), а в самой активити написать обработчик.

    <intent-filter>
        <action android:name="android.intent.action.SEND" />
        <category android:name="android.intent.category.DEFAULT" />
        <data android:mimeType="text/plain" />
    </intent-filter>
    

    Указанный выше код означает, что активити может получать аргументы с типом "android.intent.action.SEND".

    <intent-filter>
      <action android:name="android.intent.action.MAIN" />
      <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
    

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

    Как запустить эту активити из другого приложения?

    // Create the text message with a string
    Intent argvBox = new Intent();
    argvBox.setAction(Intent.ACTION_SEND);
    argvBox.setType("text/plain");
    argvBox.putExtra(Intent.EXTRA_TEXT, "<switch 1> <switch 2> <file name>");
    // Start the activity
    this.startActivity(argvBox);
    

    Код сверху очень общий и будет запускать любые активити, заявляющие, что поддерживают android.intent.action.SEND и категорией android.intent.category.DEFAULT. Наверное, можно и свои собственные типы сообщений определять, я ещё не разобрался. Код сверху можно запускать из любых функций в первой активити. Но как его обработать?

    Вот так:

    public class SecondActivity extends AppCompatActivity {
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            this.setContentView(R.layout.activity_second);
            ((TextView) this.findViewById(R.id.first_text_field)).setText("Unset in code");
            ((TextView) this.findViewById(R.id.first_text_field))
                    .setText(this.getIntent()
    /                .getStringExtra(android.content.Intent.EXTRA_TEXT));
    }}
    

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

    1. onCreate – это не конструктор. Ну, потому что в Google ненавидят RAII и считают, что конструкторы придумали глупые люди.
    2. Заранее предполагается, что активити сериализуема, поэтому на вход всегда подаётся Bundle, даже если нам вообще это не нужно.
    3. argv передаётся через "глобальную переменную" (ну, глобальную для активити), Intent, получаемый через аналог getenv, getIntent.
    4. .getStringExtra(android.content.Intent.EXTRA_TEXT) – это вообще какой-то полный маразм.

    Этот код можно написать только в onCreate, потому что ну… дебилы проектировали, чо. Несмотря на навороченный Lifecycle активити (https://developer.android.com/guide/components/activities/intro-activities#mtal), метода onReceiveIntent у неё нет.

  4. TODO Как отправить ответ обратно вопрошающему?

    Ну, мне это особенно не было нужно, мне ответ отправляли чужие активити, но чисто ради интереса:

    this.setResult (int resultCode, Intent data);
    this.finish();
    

    Внимание: на этом активити умрёт, будет уничтожена. (Смотри диаграмму https://developer.android.com/reference/android/app/Activity#activity-lifecycle )

    Почему так странно сделано? Хрен его знает, если честно. Чтобы не гонять разные активити в разных тредах?

    Заметьте, что на диаграмме нет onActivityResult. Почему? Ну, говно документация у Google, чего.

    Вот диаграмма, где он есть:

    android-lifecycle-activity-to-fragments.png
  5. TODO Как получить результат из вызова Activity

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

    Тут есть два способа.

    1. TODO startActivityForResult.

      startActivityForResult работает точно так же, как startActivity, но предполагает, что ответ нам вернут. Для примера я запрошу доступ к записи экрана.

      final int REQUEST_MEDIA_PROJECTION_CODE = 1;
      this.startActivityForResult(
          this.getSystemService(Context.MEDIA_PROJECTION_SERVICE).createScreenCaptureIntent(),
          REQUEST_MEDIA_PROJECTION_CODE);
      

      Что за чушь тут происходит?

      startActivityForResult получает на вход интент и тэг, который мы потом будем использовать для опознавания результата, который нам вернёт другая активити. Этот интент мы конструируем не сами, а просим систему подготовить для нас.

      Вопрос: если мы всё равно просим систему подготовить для нас специальный интент, который только для захвата экрана и годится, то зачем городить огород с самодельной отправкой этого интента через startActivityForResult? Ну, почему-почему, говнокод, вот почему.

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

      Отдельно стоит заметить, что про класс Context мы пока не говорили, но в данном случае это можно для ясности замять, просто упомянуть, что это пример антипаттерна "God object", через который можно вызывать много функций операционной системы.

      Теперь как получить результат запроса?

      @Override
          public void onActivityResult(int requestCode, int resultCode, Intent data) {
              super.onActivityResult(requestCode, resultCode, data);
              Log.i("onActivityResult", "receiving result");
      
              if (requestCode == REQUEST_MEDIA_PROJECTION_CODE) {
                  if (resultCode != Activity.RESULT_OK) {
                      Log.i("Permission", "Failed to get permission.");
                      return;
                  }
                  this.mProjectionPermissionResultCode = resultCode;
                  this.mProjectionPermissionResultData = data;
              }
          }
      

      Что? Это делается в другой функции? И результат можно получить только переда его через глобальные (для модуля) переменные? Но нахуя? Что это за говно вообще?

      Больше того, несмотря на то, что результат получается не просто в посторонней, а прямо таки в захардкоженной функции, диспетчеризация вызовов в Android Java асинхронная, но не параллельная. То есть, в вызывающей функции нельзя "подождать" пока ответ от второй активити вернётся и будет обработан в onActivityResult. Ну, если только самому не запускать отдельно тред.

      Что нам говорит Google на эту тему?

      When starting an activity for a result, it is possible—and, in cases of memory-intensive operations such as camera usage, almost certain—that your process and your activity will be destroyed due to low memory.

      А что будет с тредом, интересно, если Андроид и правда убъёт активити при переключении на камеру?

      В общем, без тредов, но мы попробуем сделать continuations для бедных.

      В нашей активити добавляем три глобальные переменные, через которые будем перекидывать данные, и пишем функцию:

      private static final int REQUEST_MEDIA_PROJECTION_CODE = 1;
      private static int mProjectionPermissionResultCode = -1;
      private static Intent mProjectionPermissionResultData = null;
      private static boolean mPermissionReceived = false;
      
      public void startScreenCast() {
        actuallyStartScreenCast();
      }
      @Override
        public void onActivityResult(int requestCode, int resultCode, Intent data) {
            super.onActivityResult(requestCode, resultCode, data);
            if (requestCode == REQUEST_MEDIA_PROJECTION_CODE) {
                if (resultCode != Activity.RESULT_OK) {
                    Log.i("Permission", "Failed to get permission.");
                    return;
                }
                this.mProjectionPermissionResultCode = resultCode;
                this.mProjectionPermissionResultData = data;
                this.mPermissionReceived = true;
                actuallyStartScreenCast();
            }
        }
      public void actuallyStartScreenCast() {
            MediaProjectionManager mediaProjectionManager =
                  (MediaProjectionManager)
                          this.getSystemService(Context.MEDIA_PROJECTION_SERVICE);
          if (this.mPermissionReceived == false) {
              this.startActivityForResult(
                      mediaProjectionManager.createScreenCaptureIntent(),
                      REQUEST_MEDIA_PROJECTION_CODE);
              return; // cannot do more, but will be called again when permission arrives.
          } else {
              MediaProjection mediaProjection = mediaProjectionManager.getMediaProjection(
                      mProjectionPermissionResultCode,
                      mProjectionPermissionResultData);
              assert mediaProjection != null : "Media projection is null";
              android.view.SurfaceView sv = this.findViewById(R.id.first_screen_view);
              assert sv != null : "SurfaceView is null";
              android.view.Surface s = sv.getHolder().getSurface();
              assert s != null : "Surface is null";
              DisplayMetrics metrics = new DisplayMetrics();
              this.getWindowManager().getDefaultDisplay().getMetrics(metrics); // Mutates!
              int dpi = metrics.densityDpi;
              VirtualDisplay mVirtualDisplay = mediaProjection.createVirtualDisplay("ScreenCapture",
                      sv.getWidth(), sv.getHeight(), dpi,
                      DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
                      s, null, null);
          }
      }
      

      Это работает, но в целом, этот идиотский проброс функций, как будто авторы очень хотели существования continuations, но не знали, как их сделать.

      1. startScreenCast
      2. actuallyStartScreenCast
      3. onActivityResult
      4. actuallyStartScreenCast

      Скринкаст тут выбран в силу того, что этот процесс, хотя и требует авторизации юзером, но не требует прописывания пермишшенов в манифест, что приятно, потому что мы этого ещё не проходили.

  6. TODO Activity Result API

    https://www.geeksforgeeks.org/how-to-use-activityforresultluncher-as-startactivityforresult-is-deprecated-in-android/

    So, the new API looks much better. No stupid uniform onActivityResult, and while we still cannot obtain the permission synchronously, we can do it in 2 parts instead of 4.

                    @Override
                    protected void onCreate(Bundle savedInstanceState) {
                      super.onCreate(savedInstanceState);
                      this.setContentView(R.layout.activity_main);
                      ((android.widget.Button) this.findViewById(R.id.button_capture2)).setText("Click me");
                      ((android.widget.Button) this.findViewById(R.id.button_capture2))
                          .setOnClickListener(
                              (android.view.View v) -> {
                                (this.registerForActivityResult(
                                    new ActivityResultContracts.StartActivityForResult(),
                                    result -> {
                                      if (result.getResultCode() == RESULT_OK) {
                                        assert result.getData() != null : "Getting permission failed!";
                                        Intent data = result.getData();
                                        MediaProjection m = ((MediaProjectionManager)
                                                             this.getSystemService(
                                                                 Context.MEDIA_PROJECTION_SERVICE))
                                                            .getMediaProjection(result.getResultCode(),
                                                                                result.getData());
                                        assert m != null : "Media projection is null";
                                        android.view.SurfaceView sv = this
                                                                      .findViewById(R.id.first_screen_view);
                                        assert sv != null : "SurfaceView is null";
                                        android.view.Surface s = sv.getHolder().getSurface();
                                        assert s != null : "Surface is null";
                                        DisplayMetrics metrics = new DisplayMetrics();
                                        this.getWindowManager()
                                            .getDefaultDisplay().getMetrics(metrics); // Mutates metrics!
                                        int dpi = metrics.densityDpi;
                                        VirtualDisplay mVirtualDisplay = m
                                         .createVirtualDisplay(
                                             "ScreenCapture",
                                             sv.getWidth(),
                                             sv.getHeight(),
                                             dpi,
                                             DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
                                             s, null, null);}}))
                               .launch(((MediaProjectionManager)this
                                        .getSystemService(Context.MEDIA_PROJECTION_SERVICE))
                                       .createScreenCaptureIntent());
                              });
    }
    

    Right? All in one method, all local, and the response is registered right where it is received.

    LifecycleOwner com.example.myscreengrabber.MainActivity@483cf7c is attempting to register while current state is RESUMED. LifecycleOwners must call register before they are STARTED.

    You must be kidding me, right?

    https://stackoverflow.com/questions/64476827/how-to-resolve-the-error-lifecycleowners-must-call-register-before-they-are-sta

    @Override
    protected void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      this.setContentView(R.layout.activity_main);
      ((android.widget.Button) this.findViewById(R.id.first_button)).setText("Click me");
      ((androidx.appcompat.widget.AppCompatButton) this.findViewById(R.id.first_button)).setOnClickListener(
          (android.view.View v) -> {
            Log.i("button", "lwf:clicked button");
            String argv = "My wonderful text sent in an intent";
            android.content.Intent argvBox = new android.content.Intent();
            argvBox.setAction(android.content.Intent.ACTION_SEND);
            argvBox.setType("text/plain");
            argvBox.putExtra(android.content.Intent.EXTRA_TEXT, argv);
            startActivity(argvBox);
          });
      ((android.widget.Button) this.findViewById(R.id.button_screen)).setOnClickListener(
          (android.view.View v) -> {
            todoScreenCast();
          });
      var caller =
          (this.registerForActivityResult(
              new ActivityResultContracts.StartActivityForResult(),
              result -> {
                if (result.getResultCode() == RESULT_OK) {
                  assert result.getData() != null : "Getting permission failed!";
                  Intent data = result.getData();
                  MediaProjection m = ((MediaProjectionManager)
                                       this.getSystemService(Context.MEDIA_PROJECTION_SERVICE))
                                      .getMediaProjection(result.getResultCode(),
                                                          result.getData());
                  assert m != null : "Media projection is null";
                  android.view.SurfaceView sv = this.findViewById(R.id.first_screen_view);
                  assert sv != null : "SurfaceView is null";
                  android.view.Surface s = sv.getHolder().getSurface();
                  assert s != null : "Surface is null";
                  DisplayMetrics metrics = new DisplayMetrics();
                  this.getWindowManager().getDefaultDisplay().getMetrics(metrics); // Mutates!
                  int dpi = metrics.densityDpi;
                  VirtualDisplay mVirtualDisplay =
                      m.createVirtualDisplay("ScreenCapture",
                                             sv.getWidth(),
                                             sv.getHeight(),
                                             dpi,
                                            DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
                                            s, null, null);
                }}));
      ((android.widget.Button) this.findViewById(R.id.button_capture2)).setOnClickListener(
          (android.view.View v) -> {
            caller.launch(((MediaProjectionManager)
                           this.getSystemService(Context.MEDIA_PROJECTION_SERVICE))
                          .createScreenCaptureIntent());
          });
    }
    

    Okay, this is stupid, but not criminally stupid. At least there is no "universal dispatcher" for return values.

1.7. TODO Async shit

1.7.1. TODO Bundle

Bundle – это какая-то такая штука, которая должна помогать нам восстанавливать состояние программы в случае перезапуска.

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

1.10. TODO Friends