Skip to content

Learn how to work with Android Things, Cloud Vision API, and Firebase and create smart doorbell

Notifications You must be signed in to change notification settings

Mobilatorium/smart-doorbell-codelab

Repository files navigation

IoT Android Things Codelab - Smart Doorbell

Пройдя данный codelab, вы познакомитесь с основами разработки IoT девайсов, основанных на Android Things. Основой для codelab послужил пример Google Cloud Doorbell. Codelab расчитан на участников, которые уже знакомы с разработкой под Android.

В итоге мы создадим девайс, который фотографирует всех, кто нажал на дверной звонок, сохраняет фотографию в Firebase и аннотирует ее с помощью Google Vision, а также приложение, позволяющее посмотреть всех посетителей.

Для прохождения codelab вам понадобится:

  • ноутбук с установленной Android Studio,
  • плата Raspberry Pi 3 с установленной Android Things,
  • Raspberry Pi совместимая камера,
  • тактовая кнопка,
  • резистор на 1кОм,
  • макетная плата,
  • набор соеденительных проводов.

Шаг 0. Подготовка.

Этап установки Android things на Raspberry Pi, подключения к WiFi и подключения из Android Studio через adb connect к устройству подробно описан в документации от Google.

Шаг 1. Создание проекта.

Перед тем, как работать с Android Things, вам необходимо обновить SDK и SDK tools до версии 26 или выше. Вы должны:

1. Создать проект для мобильного устройства с API 26 с пустым Activity.

2. Добавить зависимость в app-level build.gradle. Обратите внимание, что зависимость предоставляется, а не компилируется. Это связано с тем, что для каждой Android Things совместимой платы используется своя реализация с общим интерфейсом:

dependencies {
    ...
    provided 'com.google.android.things:androidthings:0.5.1-devpreview'
}

3. Добавить запись об используемой библиотеке в манифест:

<application ...>
    <uses-library android:name="com.google.android.things"/>
    ...
</application>

4. Добавить записи в манифест, позволяющие запускать IoT приложение при загрузке девайса (именно добавить, стандартный intent-filter позволит запускать приложение при деплое и дебаге).

<application
    android:label="@string/app_name">
    ...
    <activity android:name=".DoorbellActivity">
        ...
        <!-- Launch activity automatically on boot -->
        <intent-filter>
            <action android:name="android.intent.action.MAIN"/>
            <category android:name="android.intent.category.IOT_LAUNCHER"/>
            <category android:name="android.intent.category.DEFAULT"/>
        </intent-filter>
        ...
    </activity>
</application>

5. После прохождения этого этапа у вас должно получиться что-то похожее.

Шаг 2. Подключение кнопки.

В качестве дверного звонка будет выступать тактовая кнопка, закрепленная на макетной плате. Для обработки нажатий кнопки к Android Things устройству можно воспользоваться классом Android Things SDK Gpio, но в этом случае нам придется обрабатывать дребезг кнопки, возникающий вследствие несовершенства физического мира, самостоятельно. Чтобы этого избежать, воспользуемся уже готовой библиотекой для работы с кнопками. Библиотеки для работы с физическими компонентами в Android Things называются драйверами. Примеры готовых драйверов можно увидеть в официальном репозитории Android Things.

Таким образом для подключения к Android Things кнопки нам потребуется:

1. С помощью соеденительных проводов, макетной платы, резистора и кнопки собрать следующую схему схема подключения кнопки к Android Things на Raspberry Pi Будьте внимательны, не перепутайте выводы 3.3V и 5V, вам необходим вывод 3.3V.

2. Подключить зависимость драйвера в вашем build.gradle уровня приложения.

dependencies {
    ...

    compile 'com.google.android.things.contrib:driver-button:0.4'
}

3. Создать объект типа Button и инициализировать его, указав, что срабатывание происхоисходит при подаче на порт высокого уровня сигнала.

import com.google.android.things.contrib.driver.button.Button;

// Access the Button and listen for events:

Button button = new Button(gpioPinName,
        // high signal indicates the button is pressed
        // use with a pull-down resistor
        Button.LogicState.PRESSED_WHEN_HIGH
);

gpioPinName - string аргумент, задающий название порта, к которому вы подключили кнопку. Перечень всех доступных портов для Raspberry Pi вы можете увидеть на схеме.

4. Прикрепить к созданной кнопки слушателя событий:

button.setOnButtonEventListener(new OnButtonEventListener() {
    @Override
    public void onButtonEvent(Button button, boolean pressed) {
        // do something awesome
    }
});

5. И не забыть освободить ресурсы, когда они нам перестануть быть нужны (в onDestroy).

button.close();

6. После прохождения этого этапа у вас должно получиться что-то похожее.

Шаг 3. Подключение камеры.

Для фотографирования посетителей будет использоваться совместимая Rapsberry Pi камера. Она шлейфом подключается в специальный разъем на Raspberry Pi. Подключение необходимо проводить к выключенной Rapsberry Pi.

Для взаимодействия с камерой из Android Things приложения необходимо:

1. Указать в манифесте приложения разрешение на использование камеры:

<uses-permission android:name="android.permission.CAMERA" />

Обратите внимание, что зачастую у Android Things устройства нет экрана для подтверждения разрешения, поэтому все разрешения принимаются автоматически при установке приложения. На данный момент существует проблема, что для получения разрешения может потребоваться перезагрузка Raspberry Pi.

2. Указать в манифесе приложения требования к наличию камеры:

<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus" />

3. Создать отдельный поток для обработки операций ввода\вывода:

public class DoorbellActivity extends Activity {

    /**
     * A Handler for running tasks in the background.
     */
    private Handler backgroundHandler;
    /**
     * An additional thread for running tasks that shouldn't block the UI.
     */
    private HandlerThread backgroundThread;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        startBackgroundThread();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();

        backgroundThread.quitSafely();
    }

    /**
     * Starts a background thread and its Handler.
     */
    private void startBackgroundThread() {
        backgroundThread = new HandlerThread("InputThread");
        backgroundThread.start();
        backgroundHandler = new Handler(backgroundThread.getLooper());
    }
}

4. Инициализировать камеру. Для этого необходимо получить список ID всех доступных камер с помощью CameraManager, создать объект ImageReader для обработки информации с камеры и открыть соединение с камерой. На этом этапе использются стандратные Android объекты, поэтому сложностей возникнуть не должно. Удобно выделить это в отдельный класс.

public class DoorbellCamera {

    // Camera image parameters (device-specific)
    private static final int IMAGE_WIDTH  = ...;
    private static final int IMAGE_HEIGHT = ...;
    private static final int MAX_IMAGES   = ...;

    // Image result processor
    private ImageReader imageReader;
    // Active camera device connection
    private CameraDevice cameraDevice;
    // Active camera capture session
    private CameraCaptureSession captureSession;

    // Initialize a new camera device connection
    public void initializeCamera(Context context,
                                 Handler backgroundHandler,
                                 ImageReader.OnImageAvailableListener imageListener) {

        // Discover the camera instance
        CameraManager manager = (CameraManager) context.getSystemService(CAMERA_SERVICE);
        String[] camIds = {};
        try {
            camIds = manager.getCameraIdList();
        } catch (CameraAccessException e) {
            Log.d(TAG, "Cam access exception getting IDs", e);
        }
        if (camIds.length < 1) {
            Log.d(TAG, "No cameras found");
            return;
        }
        String id = camIds[0];

        // Initialize image processor
        imageReader = ImageReader.newInstance(IMAGE_WIDTH, IMAGE_HEIGHT,
                ImageFormat.JPEG, MAX_IMAGES);
        imageReader.setOnImageAvailableListener(imageListener, backgroundHandler);

        // Open the camera resource
        try {
            manager.openCamera(id, mStateCallback, backgroundHandler);
        } catch (CameraAccessException cae) {
            Log.d(TAG, "Camera access exception", cae);
        }
    }

    private static class InstanceHolder {
        private static DoorbellCamera mCamera = new DoorbellCamera();
    }
    
    static DoorbellCamera getInstance() {
        return InstanceHolder.mCamera;
    }
    
    // Callback handling devices state changes
    private final CameraDevice.StateCallback mStateCallback =
            new CameraDevice.StateCallback() {

        @Override
        public void onOpened(CameraDevice cameraDevice) {
            DoorbellCamera.this.cameraDevice = cameraDevice;
        }

        ...
    };

    // Close the camera resources
    public void shutDown() {
        if (cameraDevice != null) {
            cameraDevice.close();
        }
    }
}

6. Реализовать метод для фотографирования. Он будет вызываться по нажатию на кнопку-звонок. Сессию фотографирования можно реализовать с помощью CameraCaptureSession, в качестве surface необходимо передать surface ImageReader, созданного ранее, а в StateCallback реагировать на успешное\не успешное конфигурирование сессии.

public class DoorbellCamera {

    ...

    public void takePicture() {
        if (cameraDevice == null) {
            Log.w(TAG, "Cannot capture image. Camera not initialized.");
            return;
        }

        // Here, we create a CameraCaptureSession for capturing still images.
        try {
            cameraDevice.createCaptureSession(
                    Collections.singletonList(imageReader.getSurface()),
                    mSessionCallback,
                    null);
        } catch (CameraAccessException cae) {
            Log.d(TAG, "access exception while preparing pic", cae);
        }
    }

    // Callback handling session state changes
    private final CameraCaptureSession.StateCallback mSessionCallback =
            new CameraCaptureSession.StateCallback() {

        @Override
        public void onConfigured(CameraCaptureSession cameraCaptureSession) {
            // When the session is ready, we start capture.
            captureSession = cameraCaptureSession;
            triggerImageCapture();
        }

        @Override
        public void onConfigureFailed(CameraCaptureSession cameraCaptureSession) {
            Log.w(TAG, "Failed to configure camera");
        }
    };
}

7. Реализовать метод для фотографирования. Для этого будем использовать CaptureRequest, surface созданного ранее ImageReader. По завершении фотографирования необходимо освободить ресурсы (закрыть сессию).

public class DoorbellCamera {

    // Image result processor
    private ImageReader imageReader;
    // Active camera device connection
    private CameraDevice cameraDevice;
    // Active camera capture session
    private CameraCaptureSession captureSession;

    ...

    private void triggerImageCapture() {
        try {
            final CaptureRequest.Builder captureBuilder =
                    cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);
            captureBuilder.addTarget(imageReader.getSurface());
            captureBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON);

            captureSession.capture(captureBuilder.build(), mCaptureCallback, null);
        } catch (CameraAccessException cae) {
            Log.d(TAG, "camera capture exception");
        }
    }

    // Callback handling capture progress events
    private final CameraCaptureSession.CaptureCallback mCaptureCallback =
        new CameraCaptureSession.CaptureCallback() {
            ...

            @Override
            public void onCaptureCompleted(CameraCaptureSession session,
                                           CaptureRequest request,
                                           TotalCaptureResult result) {
                if (session != null) {
                    session.close();
                    captureSession = null;
                    Log.d(TAG, "CaptureSession closed");
                }
            }
        };
}

8. Для использования фотографии c Google Cloud Vision удобно ее сериализовать, представив в виде массива байтов. Это удобно делать в Activity, а не в DoorbellCamera классе, так как в дальнейшем эта информация будет передаваться для использования с GoogleVision.

public class DoorbellActivity extends Activity {

    /**
     *  Camera capture device wrapper
     */
    private DoorbellCamera camera;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ...

        camera = DoorbellCamera.getInstance();
        camera.initializeCamera(this, backgroundHandler, mOnImageAvailableListener);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        ...

        camera.shutDown();
    }

    private Button.OnButtonEventListener mButtonCallback =
            new Button.OnButtonEventListener() {
        @Override
        public void onButtonEvent(Button button, boolean pressed) {
            if (pressed) {
                // Doorbell rang!
                camera.takePicture();
            }
        }
    };

    // Callback to receive captured camera image data
    private ImageReader.OnImageAvailableListener mOnImageAvailableListener =
            new ImageReader.OnImageAvailableListener() {
        @Override
        public void onImageAvailable(ImageReader reader) {
            // Get the raw image bytes
            Image image = reader.acquireLatestImage();
            ByteBuffer imageBuf = image.getPlanes()[0].getBuffer();
            final byte[] imageBytes = new byte[imageBuf.remaining()];
            imageBuf.get(imageBytes);
            image.close();

            onPictureTaken(imageBytes);
        }
    };

    private void onPictureTaken(byte[] imageBytes) {
        if (imageBytes != null) {
            // Compress image
            Bitmap bitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length);
            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
            bitmap.compress(Bitmap.CompressFormat.JPEG, 90, byteArrayOutputStream);
            final byte[] imageBytesCompressed = byteArrayOutputStream.toByteArray();
            ...
        }
    }
}

9. На этом этапе ваше устройство уже может фотографировать. Для проверки этого можете подключить к Raspberry Pi монитор, реализовать в main_layout ImageView и отображать то, что вы сфотографировали на нем, но для прохождения codelab - это не обязательно, в дальнейшем мы будем отображать сделанный снимок на экране вспомогательного устройства.

Bitmap bitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length);
imageView.setImageBitmap(bitmap);

10. После прохождения этого этапа у вас должно получиться что-то похожее.

Шаг 4. Подключение Google Cloud Vision и анализ изображения.

Google Cloud Vision предоставляет широкий перечень инструментов для обработки изображений. В данном codelab мы будем использовать аннотирование с помощью меток. Это позволит нам выяснить, что находится на изображении. Для работы с ним вам потребуется Cloud Vision API key. Тестовый API key вы можете получить у организаторов codelab. В случае если вы проходите codelab самостоятельно - вам потребуется зарегистрироваться в Cloud Vision, создать свой проект и сгенерировать ключ для Android приложения. Обратите внимание, что обработка изображения и запрос НЕ должны выполняться в главном потоке.

Далее вам необходимо:

1. Добавить в проект на уровне приложения следующие зависимости:

dependencies {
    ...

    compile 'com.google.api-client:google-api-client-android:1.22.0' exclude module: 'httpclient'
    compile 'com.google.http-client:google-http-client-gson:1.22.0' exclude module: 'httpclient'

    compile 'com.google.apis:google-api-services-vision:v1-rev22-1.22.0'
}

2. Добавить в манифест приложения разрешение на использование интернета. Обратите внимание, что для получения разрешения может потребоваться перезагрузка Android Things устройства.

<uses-permission android:name="android.permission.INTERNET" />

3. Создать VisionRequestInitializer с помощью полученного ранее ключа.

// Construct the Vision API instance
    HttpTransport httpTransport = AndroidHttp.newCompatibleTransport();
    JsonFactory jsonFactory = GsonFactory.getDefaultInstance();
    VisionRequestInitializer initializer = new VisionRequestInitializer(CLOUD_VISION_API_KEY);

4. Создать новый объект Vision.

Vision vision = new Vision.Builder(httpTransport, jsonFactory, null)
        .setVisionRequestInitializer(initializer)
        .build();

5. Создать новый объект запроса AnnotateImageRequest и добавить к нему описание запроса с помощью объекта типа Feature.

// Create the image request
AnnotateImageRequest imageRequest = new AnnotateImageRequest();
Image image = new Image();
image.encodeContent(imageBytesCompressed);
imageRequest.setImage(image);

// Add the features we want
Feature labelDetection = new Feature();
labelDetection.setType("LABEL_DETECTION");
labelDetection.setMaxResults(10);
imageRequest.setFeatures(Collections.singletonList(labelDetection));

6. Создать и запустить сам запрос с помощью объекта BatchAnnotateImagesRequest.

// Batch and execute the request
BatchAnnotateImagesRequest requestBatch = new BatchAnnotateImagesRequest();
requestBatch.setRequests(Collections.singletonList(imageRequest));
BatchAnnotateImagesResponse response = vision.images()
        .annotate(requestBatch)
        .setDisableGZipContent(true)
        .execute();

7. В результате выполнения запроса мы получим объект типа BatchAnnotateImagesResponse. Для нашей задачи нам необходим только список аннотаций, поэтому напишем небольшой метод для конвертации из BatchAnnotateImagesResponse в Map<String, Float>.

private Map<String, Float> convertResponseToMap(BatchAnnotateImagesResponse response) {
    Map<String, Float> annotations = new HashMap<>();

    // Convert response into a readable collection of annotations
    List<EntityAnnotation> labels = response.getResponses().get(0).getLabelAnnotations();
    if (labels != null) {
        for (EntityAnnotation label : labels) {
            annotations.put(label.getDescription(), label.getScore());
        }
    }

    return annotations;
}

8. Вызываем метод, реализующий запрос к Google Vision API из созданного на предыдущем шаге метода onPictureTaken и передаем ему в качестве аргумента байтовый массив, описывающий фотографию. Таким образом, мы получим список аннотаций для дальнейшего использования.

9. После прохождения этого этапа у вас должно получиться что-то похожее.

Шаг 5. Сохранение информации в Firebase.

Для работы с Firebase, вам потребуется google account и созданный Firebase проект. Вы сможете это сделать из firebase console.

1. Создать новый проект и добавить к проекту Android приложение. Эти операции очень просто сделать из firebase console.

2. Скачать google-services.json файл и сохранить его в папке с вашим android приложением.

3. Добавить зависимости в build.gradle на уровне проекта:

buildscript {
  dependencies {
    ...
    classpath 'com.google.gms:google-services:3.0.0'
  }
}

И на уровне приложения:

dependencies {
    ...

    compile 'com.google.firebase:firebase-core:9.6.1'
    compile 'com.google.firebase:firebase-database:9.6.1'
}

4. Установить разрешения для чтения\записи в базу данных. Правила доступа к БД указываются в firebase console в разделе База данных - Правила. Для codelab достаточно разрешить чтение и запись из любых источников.

{
  "rules": {
    ".read": true,
    ".write": true
  }
}

5. Firebase будет хранить информацию обо всех событиях нажатия на звонок. Предлагаемая структура сохраняемого эвента следующая:

<doorbell-entry> {
    "image": <Base64 image data>,
    "timestamp": <event timestamp>,
    "annotations": {
        <label>: <score>,
        <label>: <score>,
        ...
    }
}

Для этого необходимо инициализировать объект FirebaseDatabase и сохранять все значения с помощью объекта DatabaseReference.

FirebaseDatabase database;

...

database = FirebaseDatabase.getInstance();

...

private void onPictureTaken(byte[] imageBytes) {
    if (imageBytes != null) {
        try {
            // Compress image
            Bitmap bitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length);
            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
            bitmap.compress(Bitmap.CompressFormat.JPEG, 90, byteArrayOutputStream);
            final byte[] imageBytesCompressed = byteArrayOutputStream.toByteArray();
            
            // Process the image using Cloud Vision
            Map<String, Float> annotations = annotateImage(imageBytesCompressed);

            // Write the contents to Firebase
            final DatabaseReference log = database.getReference("logs").push();
            log.child("timestamp").setValue(ServerValue.TIMESTAMP);

            // Save image data Base64 encoded
            String encoded = Base64.encodeToString(imageBytesCompressed,
                    Base64.NO_WRAP | Base64.URL_SAFE);
            log.child("image").setValue(encoded);

            if (annotations != null) {
                log.child("annotations").setValue(annotations);
            }
        } catch (IOException e) {
            Log.w(TAG, "Unable to annotate image", e);
        }
    }
}

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

Шаг 6. Парное Android приложение.

Не у всех Android Things устройств есть устройства ввода\вывода (дисплей, клавиатура и т. д .), поэтому для отображения подробной информации о событиях и настройки устройства целесообразно использовать отдельные приложения, к примеру Android приложение. На этом шаге мы создадим Android приложение для отображения событий звонка в дверь. Для этого необходимо:

1. Создать новый модуль в проекте и добавить firebase зависимости к модулю на уровне приложения:

dependencies {
    ...

    compile 'com.google.firebase:firebase-core:11.4.2' 
    compile 'com.google.firebase:firebase-database:11.4.2' 
    compile 'com.firebaseui:firebase-ui-database:0.5.3'
}

А также убедитесь, что на уровне проекта добавлена следующая зависимость:

buildscript {
  dependencies {
    ...
    classpath 'com.google.gms:google-services:3.0.0'
  }
}

По сравнению с шагом № 5 мы добавили еще вспомогательную библиотеку firebase-ui-database, позволяющую проще отображать UI элементы, зависящие от Firebase.

2. Добавить к новому модулю файл google-services.json. Обратите внимание на то, что в случае если application id будет отличаться - вам необходимо добавить новое приложение в firebase console и скачать новый google-services.json.

3. Для взаимодействия с Firebase необходимо создать класс-модель, описывающий структуру хранимого события.

public class DoorbellEntry {

    Long timestamp;
    String image;
    Map<String, Float> annotations;

    public DoorbellEntry() {
    }

    public DoorbellEntry(Long timestamp, String image, Map<String, Float> annotations) {
        this.timestamp = timestamp;
        this.image = image;
        this.annotations = annotations;
    }

    public Long getTimestamp() {
        return timestamp;
    }

    public String getImage() {
        return image;
    }

    public Map<String, Float> getAnnotations() {
        return annotations;
    }
}

4. FirebaseRecyclerAdapter из библиотеки FirebaseUI позволит очень просто отображать содержимое нашего массива событий в RecyclerView. По необходимости имплементация метода populateViewHolder будет наполнять ViewHolder для каждого из хранимых событий.

public class DoorbellEntryAdapter extends FirebaseRecyclerAdapter<DoorbellEntry, DoorbellEntryAdapter.DoorbellEntryViewHolder> {

    /**
     * ViewHolder for each doorbell entry
     */
    static class DoorbellEntryViewHolder extends RecyclerView.ViewHolder {

        public final ImageView image;
        public final TextView time;
        public final TextView metadata;

        public DoorbellEntryViewHolder(View itemView) {
            super(itemView);

            this.image = (ImageView) itemView.findViewById(R.id.imageView1);
            this.time = (TextView) itemView.findViewById(R.id.textView1);
            this.metadata = (TextView) itemView.findViewById(R.id.textView2);
        }
    }

    private Context applicationContext;

    public DoorbellEntryAdapter(Context context, DatabaseReference ref) {
        super(DoorbellEntry.class, R.layout.doorbell_entry, DoorbellEntryViewHolder.class, ref);

        applicationContext = context.getApplicationContext();
    }

    @Override
    protected void populateViewHolder(DoorbellEntryViewHolder viewHolder, DoorbellEntry model, int position) {
        // Display the timestamp
        CharSequence prettyTime = DateUtils.getRelativeDateTimeString(applicationContext,
                model.getTimestamp(), DateUtils.SECOND_IN_MILLIS, DateUtils.WEEK_IN_MILLIS, 0);
        viewHolder.time.setText(prettyTime);

        // Display the image
        if (model.getImage() != null) {
            // Decode image data encoded by the Cloud Vision library
            byte[] imageBytes = Base64.decode(model.getImage(), Base64.NO_WRAP | Base64.URL_SAFE);
            Bitmap bitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length);
            if (bitmap != null) {
                viewHolder.image.setImageBitmap(bitmap);
            } else {
                Drawable placeholder =
                        ContextCompat.getDrawable(applicationContext, R.drawable.ic_image);
                viewHolder.image.setImageDrawable(placeholder);
            }
        }

        // Display the metadata
        if (model.getAnnotations() != null) {
            ArrayList<String> keywords = new ArrayList<>(model.getAnnotations().keySet());

            int limit = Math.min(keywords.size(), 3);
            viewHolder.metadata.setText(TextUtils.join("\n", keywords.subList(0, limit)));
        } else {
            viewHolder.metadata.setText("no annotations yet");
        }
    }

}

В качестве ic_image вы можете использовать любую иконку/плейсхолдер.

Приведенный ViewHolder наполняет следующий layout:

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:orientation="horizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:paddingTop="@dimen/activity_vertical_margin"
        android:paddingBottom="@dimen/activity_vertical_margin">

        <ImageView
            android:layout_width="220dp"
            android:layout_height="match_parent"
            android:id="@+id/imageView1" />

        <LinearLayout
            android:orientation="vertical"
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:layout_margin="10dp">

            <TextView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:textStyle="bold"
                android:id="@+id/textView1" />

            <TextView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:id="@+id/textView2" />
        </LinearLayout>
    </LinearLayout>

5. Создав RecyclerView и установив для него созданный DoorbellEntryAdapter, мы сможем отображать сделанные фотографии и полученные с помощью Google Cloud Vision аннотации. Пример получившегося кода.

About

Learn how to work with Android Things, Cloud Vision API, and Firebase and create smart doorbell

Topics

Resources

Stars

Watchers

Forks

Packages

No packages published

Languages