안드로이드 웨어 코드랩 - XYZ Tourist Attractions

Created: Aug, 2014

Author: TI Chang

Doc link: goo.gl/0HHvYZ (or view the web version)

Introduction

이 코드랩은 Android Wear 플랫폼을 위한 개발의 다양한 부분을 다루고 있습니다. 여기서 설명할 기능과 API는 다음과 같습니다.

코드랩을 완료하는데 2-4시간 정도 걸릴 예정입니다.

The “XYZ Tourist Attractions” App

이 샘플앱은 여행 도우미 앱으로 Google Play ServicesFused Location ProviderGeofencing API이용해서 유저의 위치와 근처의 여행지를 알아냅니다. 만일 유저가 이 앱을 이용하지 않고 있다면

이 프로젝트는 몇개의 지역을 등록한 static 데이터를 이용하고 있으며 메뉴옵션을 통해서 유저의 위치에 관계 없이 코드랩을 진행하고 테스트 할 수 있습니다. 실제 geofencing은 해당 위치에 있지 않으면 동작하지 않습니다.

Data provider는 static하게 만들어 졌지만 이미지들은 네트워크를 통해 다운 받은 것을 사용하게 되어 있습니다.

Quick Tips

안드로이드 스튜디오에서 코드랩이 쉽게 동작하게 하는 두가지 팁이 있습니다.

CodeLab Steps

Step 0 - 셋업과 사전 준비

Step 1 - Notification을 보기 좋게

Step 2 - 웨어 전용 notification 개선

Step 3 - 웨어에 데이터를 보내기

Step 4 - 로컬 Notification 발생시키기

Step 5 - Data layer에서 데이터를 가져오기

Step 6 - 웨어 앱의 view를 수정하기

Step 7 - 웨어 앱에서 모바일 단말의 action을 실행하기

Step 8 - Notification 끄기 동기화

Step 9 - 사각형과 원형 화면 최적화

Step 10 - 완료하셨습니다!

Step 0 - 셋업과 사전 준비

  1. 코드랩을 완료하는데 안드로이드 4.3 (Jelly Bean) 이상을 구동하는 단말이 필요하며 안드로이드 웨어 단말이 셋업되어 연결 되어 있어야 합니다. 웨어 단말을 adb를 통해 연결하는 데는 두가지 방법이 있습니다.

  1. Android Studio Beta v0.8.6 이상 버전을 사용하기 바랍니다. 여기 에서 다운 받으시고 업데이트를 받으시면 됩니다

  1. Android SDK Manager를 통해 최신 버전의 SDK를 받으셔야 합니다. 최신 버전의 Google Play Services 와 Android SDK Build-tools이 필요하며 Android 4.4W (API 20)와 Android 4.4 (API 19) 전체 플랫폼이 설치되어 있어야 합니다.

  1. git을 통해 코드랩을 다운 받습니다:

    git clone https://code.google.com/p/wear-codelab/

    zip 압축 버전은 여기에 있습니다:
    http://goo.gl/PfLkpj

  1. Android Studio로 import 합니다:
    File -> Import Project -> Choose XYZTouristAttractions directory
  2. 컴파일과 디바이스 설치가 잘 되는 지 확인 합니다 (Run -> Build ‘mobile’).

  1. Action Bar 메뉴 (오른 쪽 위) 에서 “Test Notification”을 선택해 기본 notification이 잘 동작하는 지 확인 합니다. 모바일 단말과 웨어 단말에서 notification이 잘 보이는 지 확인 합니다 .

  1. 기본적으로 앱은 common/TouristAttractions.java에서 지정한 static geofence를 이용합니다. 이 기능을 끄기 위해서는 Action Bar 메뉴에서 “Toggle Geofence Trigger”를 선택하세요. Geofence를 통해 생성된 웨어 전용 notification은 기본적으로 나타나지 않게 해두었습니다. “Test Notification” 을 이용해서 bridged notification 이 어떻게 보이는지 확인 하세요.

Step 1 - Notification을 보기 좋게

Tip:필요하다면 다음의 명령으로 이 스텝을 할 수 있게 바로 준비 할 수 있습니다:

git checkout step1_start

현재 notification은 매우 평범하며 작은 이미지만  보입니다. Step 1에서는 이 notification을 더 웨어 단말과 모바일 단말에서 매력적으로 보이게 바꿉니다.

  1. Open mobile/src/main/java/.../service/UtilityService.java 에서 showNotification() 을 찾습니다. 이 method가 geofence가 발생하거나 테스트 메뉴를 선택했을 때 notification을  보여주는 역할을 합니다.

  1. showNotification() 안에서 NotificationCompat.Builder 클래스를 이용해서 notification이 만들어지는 부분을 찾습니다.

  1. 적절한 notification style을 적용해서 모바일과 웨어 단말에서 보기 좋게 개선 합니다.

// TODO: Set BigPictureStyle style

.setStyle(new NotificationCompat.BigPictureStyle()

        .bigPicture(bitmaps.get(attraction.name))

        .setBigContentTitle(attraction.name)

        .setSummaryText(getString(R.string.nearby_attraction))

)

  1. setLargeIcon(...) 을 builder에서 제거합니다.

  1. 다시 notification을 보고 모바일과 웨어 단말에서 얼마나 잘 보이는 지 확인 합니다. 아래와 같은 화면을 보실 수 있습니다:

Step 2 - 웨어 전용 notification 개선

Tip:필요하다면 다음의 명령으로 이 스텝을 할 수 있게 바로 준비 할 수 있습니다:

git checkout step2_start

자 이제 notification이 더 보기 좋아 졌지만 추가로 정보를 더 보여주는 것이 새 도시를 여행할 때 더 유용할 것 같습니다. 웨어 단말에서는 pages 를 이용해서 가까운 여행지의 정보를 더 보여줄 수 있습니다.

  1. mobile/.../service/UtilityService.java 파일을 열어서 showNotification() method를 다시 찾습니다.

  1. NotificationCompat.Builder 클래스를 이용해서 notification이 만들어지는 부분을 찾습니다. if 문안에서 TODO 를 찾을 수 있습니다.

  1. 다음의 코드를 if 문 안에 붙여 넣도록 합니다.

// If not a micro app, create some wearable pages for

// the other nearby tourist attractions.

ArrayList<Notification> pages = new ArrayList<Notification>();

for (int i = 1; i < attractions.size(); i++) {

    // Calculate the distance from current location to tourist attraction

    String distance = Utils.formatDistanceBetween(

            Utils.getLocation(this), attractions.get(i).location);

    // Construct the notification and add it as a page

    pages.add(new NotificationCompat.Builder(this)

            .setContentTitle(attractions.get(i).name)

            .setContentText(distance)

            .setSmallIcon(R.drawable.ic_stat_icon)

            .extend(new NotificationCompat.WearableExtender()

                    .setBackground(bitmaps.get(attractions.get(i).name))

            )

            .build());

}

builder.extend(new NotificationCompat.WearableExtender().addPages(pages));

  1. 이것은 가까운 여행지를 찾아 각각 웨어 notification page를 생성해서 리스트에 넣고 WearableExtender이용해 일반적인 notification을 확장하도록 합니다.

  1. Notification을 한번더 테스트 해보겠습니다. 같은 BigPictureStyle이 모바일 단말에 나타나지만 웨어 단말에서는 한페이지당 한개의 여행지씩 여러개의 페이지가 보여야 합니다. 이미지 데이터를 받아서 웨어 단말에 전송하는 과정에서 웨어 단말에 나타나는데는 몇 초 더 걸릴 수 있습니다.

Step 3 - 웨어에 데이터를 보내기

Tip:필요하다면 다음의 명령으로 이 스텝을 할 수 있게 바로 준비 할 수 있습니다:

git checkout step3_start

Enhanced notifications은 많은 케이스에 유용하지만 좀더 유저 경험을 커스터마이즈 하려면 웨어용 커스텀 앱을 제작할 수 있습니다. 그 첫번째 단계로 웨어 단말에 필요한 데이터를 보내 보겠습니다.

  1. mobile/.../service/UtilityService.java 파일을 열고 sendDataToWearable() method를 찾습니다. 이 method는 showNotification() method에 의해 로컬 모바일 단말 notification이 만들어질 때 실행됩니다.

  1. 해당 method의 첫번째 부분을 읽습니다. Google Play Services를 셋업하고 여행지 이미지를 받아와 리사이징 하는 부분을 볼 수 있습니다 (메인 이미지와 맵 이미지).

  1. TODO 까지 스크롤 해서 내려갑니다.

  1. 여행지 리스트 루프 안에서 약간의 셋업을 합니다.

// Loop through each attraction, create the DataMap and send it

// off to the wearable

Attraction attraction = attractions.get(i);

Asset imageAsset = Utils.createAssetFromBitmap(images.get(attraction.name));

Asset mapAsset = Utils.createAssetFromBitmap(maps.get(attraction.name));

PutDataMapRequest dataMap = PutDataMapRequest.create(

        Constants.ATTRACTION_PATH + "/" + Uri.encode(attraction.name));

  1. 이 부분은 비트맵을 웨어용 Asset 으로 바꾸는데 이렇게 하면 바이너리 데이터를 웨어 네트워크를 통해 복사할 수 있게 됩니다 . 또 특별히 PutDataMapRequest 를 생성하여 구조화된 데이터를 저장할 수 있게 합니다. 여행지 마다 이름을 이용해서 유니크한 path를 하나씩 정해주는 점에 주의하시기 바랍니다.

  1. DataMap을 여행지 정보로 채웁니다.

String distance = Utils.formatDistanceBetween(

        Utils.getLocation(this), attractions.get(i).location);

dataMap.getDataMap().putString(Constants.EXTRA_TITLE, attraction.name);

dataMap.getDataMap().putString(

        Constants.EXTRA_DESCRIPTION, attraction.description);

dataMap.getDataMap().putString(Constants.EXTRA_DISTANCE, distance);

dataMap.getDataMap().putString(Constants.EXTRA_CITY, attraction.city);

dataMap.getDataMap().putAsset(Constants.EXTRA_IMAGE, imageAsset);

dataMap.getDataMap().putAsset(Constants.EXTRA_MAP, mapAsset);

PutDataRequest request = dataMap.asPutDataRequest();

  1.  Wearable.DataApi 를 이용해서 웨어 네트워크에 데이터를 보냅니다.

// Send the data over

DataApi.DataItemResult result =

        Wearable.DataApi.putDataItem(googleApiClient, request).await();

if (result.getStatus().isSuccess()) {

    // If successful store the data path

    attractionIds.add(result.getDataItem().getUri().toString());

}

  1. 전송이 성공하면 리스트에 Uri가 저장되는 점에 유의하세요. 이 리스트는 성공적으로 보내진 데이터에 다른 작업을 더 해야 할 때 웨어 네트워크를 통해 메시지를 보낼 경우 사용 될겁니다.

  1. 여행지 루프 밖에서 Wearable.MessageApi method 를 이용해 메시지를 보냅니다.

// Construct an array of all successfully sent data paths

DataMap dataMap = new DataMap();

dataMap.putStringArrayList(Constants.EXTRA_UPDATED_ATTRACTIONS, attractionIds);

// Convert to bytes to be send with the message

byte[] attractionIdBytes = dataMap.toByteArray();

Iterator<String> itr = Utils.getNodes(googleApiClient).iterator();

while (itr.hasNext()) {

    // Notify all nodes to "start", providing the data paths of all

    // transmitted tourist attractions. What "start" does will be up

    // to the wearable.

    Wearable.MessageApi.sendMessage(

            googleApiClient, itr.next(), Constants.START_PATH, attractionIdBytes);

}

  1. 메시지는 유저의 개인용 웨어 네트워크의 모든 노드에 전달 되는 점을 유의해주세요.

  1. 이 부분을 테스트하기 위해서 업데이트 된 코드를 컴파일하고 모바일 단말과 웨어 단말에 설치해 주세요. (디버그 빌드는 웨어 APK를 자동으로 설치 하지 않습니다).
  1. 먼저 모바일 앱을 모바일 단말에 설치합니다.
  2. 웨어 단말이 Step 0.1에 설명된 것 과 같이 연결 되어 있는 지 확인 하세요.
  3. 웨어 프로젝트를 웨어 단말에 설치 하세요. (Run -> ‘Run ...’ -> ‘wear’를 선택하거나 toolbar의 ‘run’ 아이콘 옆의 드롭다운 리스트에서 선택하세요.)

  1. 그리고 Action Bar 메뉴에서 “Test Micro App”을 선택하세요. 웨어 단말 로그에서 wear/.../service/ListenerService.java에서 실행되는 onDataChanged()와 onMessageReceived() 나타나는지 확인 하세요. 로그는 다음과 같이 보일 겁니다.

    V/ListenerService﹕ onDataChanged: ...
    V/ListenerService﹕ onMessageReceived: ...


    onDataChanged 로그는 각데이터가 보내질 때 한번만 보일 겁니다 (그다음에 캐싱 됩니다).
    그리고 이시점에서는 아직 웨어 단말에는 어떤 변화도 눈에 보이지는 않는 점을 유의하세요 (다음 스텝에서 진행됩니다).

Step 4 - 로컬 Notification 발생시키기

Tip:필요하다면 다음의 명령으로 이 스텝을 할 수 있게 바로 준비 할 수 있습니다:

git checkout step4_start

이제 웨어 단말에서 받게 되는 메시지와 데이터에 작업을 하겠습니다. 로컬 Notification을 추가 하는 것 부터 시작하겠습니다.

메시지로 받은 데이터를 풉니다 (이것은 Step 3.8에서 성공적으로 전송된 패스들의 리스트입니다). 그리고 notification에 action으로 전달될 Intent와 PendingIntent 를 만듭니다. 이것은 웨어 단말의 로컬 Activity 인 AttractionsActivity를 실행 시킵니다. 어떻게 패스들의 리스트가 Intent의 extra를 통해 전달되어 Activity에서 이 리스트에 접근 가능한지 살펴 보시면 좋을 것 같습니다.

 

  1. ‘wear’ 프로젝트에서 wear/src/main/java/.../service/ListenerService.java 파일을 열어 onMessageReceived() method 를 찾습니다. method의 맨 끝에 있는 TODO 항목까지 스크롤 해 내려갑니다.

// TODO: Unparcel the data and then construct an Intent and PendingIntent for the local Activity

ArrayList<String> attractions =

        dataMap.getStringArrayList(Constants.EXTRA_UPDATED_ATTRACTIONS);

Intent intent = new Intent(this, AttractionsActivity.class);

intent.putExtra(Constants.EXTRA_UPDATED_ATTRACTIONS, attractions);

PendingIntent pendingIntent =

        PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);

  1. 리스트에서 첫 여행지 데이터를 로드 합니다. 로드 된 데이터 중 이미지는 notification의 배경 이미지로 쓰게 됩니다.

// TODO: Load the data from the first (closest) tourist attraction

int count = attractions.size();

Uri uri = Uri.parse(attractions.get(0));

DataApi.DataItemResult dataItemResult =

        Wearable.DataApi.getDataItem(googleApiClient, uri).await();

DataMap attractionDataMap =

        DataMapItem.fromDataItem(dataItemResult.getDataItem()).getDataMap();

Bitmap bitmap = Utils.loadBitmapFromAsset(

        googleApiClient, attractionDataMap.getAsset(Constants.EXTRA_IMAGE));

  1. Notification을 만들고 보냅니다. 이 것은 웨어 앱을 웨어 단말에서 구동하는 데 쓰이기 때문에 하위 호환을 신경 쓸 필요가 없어서 suppport library의 NotificationCompat아닌 플랫폼 notification 클래스를 이용하였습니다.

// TODO: Construct and trigger the notification

Notification notification = new Notification.Builder(this)

        .setContentText(

                getResources().getQuantityString(R.plurals.attractions_found, count, count))

        .setSmallIcon(R.drawable.ic_launcher)

        .addAction(R.drawable.ic_full_explore,

                getString(R.string.action_explore),

                pendingIntent)

        .extend(new Notification.WearableExtender()

                .setBackground(bitmap)

        )

        .build();

// Show the notification

NotificationManager notificationManager =

        (NotificationManager) getSystemService(NOTIFICATION_SERVICE);

notificationManager.notify(Constants.WEAR_NOTIFICATION_ID, notification);

  1. addAction() 이 바로 전에 만든 Intent를 이용해서 로컬 Activity를 실행시키는데 사용 됩니다.

  1. 최신 코드를 웨어 단말에 설치하고 모바일 앱의 “Test Micro App”를 이용해 테스트 합니다. 이제 웨어 단말에서 아래와 같은 화면을 볼 수 있습니다. “Explore”를 누르면 검은 화면이 보이게 됩니다. 왼쪽으로 swipe 하면 종료할 수 있습니다.

Step 5 - Data layer에서 데이터를 가져오기

Tip:필요하다면 다음의 명령으로 이 스텝을 할 수 있게 바로 준비 할 수 있습니다

git checkout step5_start

커스터마이즈된 웨어 Activity에서 pager안에 adapter를 이용해서 표시하도록 data layer를 이용해 여행지 정보를 읽어오려합니다.

  1. wear/.../ui/AttractionsActivity.java 파일에서 FetchDataAsyncTask 이너 클래스를 찾습니다. (이 task가 어떻게 불리는 지 알기 위해서는 AsyncTask 문서를 참고 하시면 됩니다). 이 백그라운드 task가 DataApi 를 이용해서 데이터를 받아 옵니다.

  1. doInBackground() method 를 찾아 data를 추출하고 Attraction 오브젝트를 다시 만들어 리스트에 저장합니다.

// TODO: Extract data from wearable data layer and store in mAttractions to be

//       passed to the GridPagerAdapter

DataApi.DataItemResult dataItemResult =

        Wearable.DataApi.getDataItem(mGoogleApiClient, uri).await();

DataItem dataItem = dataItemResult.getDataItem();

if (dataItem != null) {

    DataMap attractionDataMap =

            DataMapItem.fromDataItem(dataItem).getDataMap();

    Attraction attraction = new Attraction();

    attraction.name = attractionDataMap.getString(Constants.EXTRA_TITLE);

    attraction.description =

            attractionDataMap.getString(Constants.EXTRA_DESCRIPTION);

    attraction.city = attractionDataMap.get(Constants.EXTRA_CITY);

    attraction.distance =

            attractionDataMap.getString(Constants.EXTRA_DISTANCE);

    attraction.image = Utils.loadBitmapFromAsset(

            mGoogleApiClient, attractionDataMap.getAsset(Constants.EXTRA_IMAGE));

    attraction.map = Utils.loadBitmapFromAsset(

            mGoogleApiClient, attractionDataMap.getAsset(Constants.EXTRA_MAP));

    mAttractions.add(attraction);

}

  1. 이번 스텝은 여기 까지입니다. 이 스텝에는 눈에 보이는 결과는 없습니다. 로그를 추가로 넣어서 잘 동작하는지 확인 하실 수 있습니다만 다음 스텝에서 그 결과를 보실 수 있습니다.

Step 6 - 웨어 앱의 view를 수정하기

Tip: 필요하다면 다음의 명령으로 이 스텝을 할 수 있게 바로 준비 할 수 있습니다.

git checkout step6_start

웨어 앱은 정상적인 안드로이드 View들을 사용할 수 있지만 wearable UI library 와 추가적인 View 및 ViewGroup 상속 클래스들도 이용할 수 있습니다. 여기에서는 여행지 정보를 세로 페이지들에 보여주고 각 여행지 정보를 가로 페이지에 보여주는데 GridViewPage를 사용하려고 합니다.

  1. wear/.../ui/AttractionsGridPagerAdapter.java 파일을 열어 instantiateItem() method를 찾습니다. 이 method 는 GridViewPager의 각 아이템을 만드는 역할을 합니다.

  1. 컬럼 1과 2에 대해서 같은 layout을 이용하고 컨텐츠에 따라 약간 조정하도록 하겠습니다. 컬럼 1에서는 여행지의 이미지를 이름과 함께 보여주도록 하고 컬럼 2에서는 맵이미지를 유저로부터의 거리와 함께 보여주도록 하겠습니다.

if (column == 0 || column == 1) {

    // TODO: Inflate and set up layout for columns 1 & 2

    // The first page is the full screen image, the second is the full screen map

    View view = mLayoutInflater.inflate(

            R.layout.gridpager_fullscreen_image, container, false);

    ImageView imageView = (ImageView) view.findViewById(R.id.imageView);

    TextView textView = (TextView) view.findViewById(R.id.textView);

    // TODO: Adjust layout params to account for different screen shapes (Step 9)

    if (column == 0) {

        imageView.setImageBitmap(attraction.image);

        textView.setText(attraction.name);

    } else {

        imageView.setImageBitmap(attraction.map);

        textView.setText(mContext.getString(R.string.map_caption, attraction.distance));

    }

    container.addView(view);

    return view;

} else if (column == 2) {

  1. 컬럼 3에는 wearable UI library에 있는 CardScrollView 와 CardFrame을 이용한 다른 layout을 이용하도록 하겠습니다.

} else if (column == 2) {

    // TODO: Inflate and set up layout for column 3

    // The description card page

    CardScrollView cardScrollView = (CardScrollView) mLayoutInflater.inflate(

            R.layout.gridpager_card, container, false);

    TextView textView = (TextView) cardScrollView.findViewById(R.id.textView);

    textView.setText(attraction.description);

    cardScrollView.setCardGravity(Gravity.BOTTOM);

    cardScrollView.setExpansionEnabled(true);

    cardScrollView.setExpansionDirection(CardFrame.EXPAND_DOWN);

    cardScrollView.setExpansionFactor(10);

    container.addView(cardScrollView);

    return cardScrollView;

} else if (column == 3) {

  1. 컬럼 4에서는 보통의 웨어 notification action과 비슷한 action 버튼 layout을 로드 합니다. 여기에서 “navigate by walking” action이  모바일 단말로 메시지를 보내고 wearable activity를 종료합니다. 이 action은 확인 action을 갖는게 좋은데 이 부분은 코드랩에서 다루지 않으니 추후 직접 해보시면 좋겠습니다.

} else if (column == 3) {

    // TODO: Inflate and set up layout for column 4

    // The navigate action

    View view = mLayoutInflater.inflate(R.layout.gridpager_action, container, false);

    ImageView imageView = (ImageView) view.findViewById(R.id.imageView);

    imageView.setImageResource(R.drawable.ic_full_directions_walking);

    TextView textView = (TextView) view.findViewById(R.id.textView);

    textView.setText(R.string.action_navigate);

    // TODO: Adjust padding to account for different screen shapes (Step 9)

    view.setOnClickListener(new View.OnClickListener() {

        @Override

        public void onClick(View v) {

            UtilityService.startDeviceActivity(mContext,

                    Constants.START_NAVIGATION_PATH, attraction.name, attraction.city);

            UtilityService.clearNotification(mContext);

            // TODO: add "confirmation_animation"

            ((Activity)mContext).finish();

        }

    });

    container.addView(view);

    return view;

} else {

  1. 마직막으로 컬럼 5에서는 “open on device”로 설정된 action button layout을 로드하도록 합니다. 이전 action과 비슷하지만 이번에는 확인 애니메이션으로 “go_to_phone_animation”을 실행합니다.

} else {

    // The "open on device" action

    View view = mLayoutInflater.inflate(R.layout.gridpager_action, container, false);

    ImageView imageView = (ImageView) view.findViewById(R.id.imageView);

    imageView.setImageResource(R.drawable.ic_full_open_on_device);

    TextView textView = (TextView) view.findViewById(R.id.textView);

    textView.setText(R.string.action_open);

    // TODO: Adjust padding to account for different screen shapes (Step 9)

    view.setOnClickListener(new View.OnClickListener() {

        @Override

        public void onClick(View v) {

            UtilityService.clearNotification(mContext);

            UtilityService.startDeviceActivity(mContext,

                    Constants.START_ATTRACTION_PATH, attraction.name, attraction.city);

            // TODO: add "go_to_phone_animation" confirmation animation

        }

    });

    container.addView(view);

    return view;

}

  1. 작업한 결과물을 웨어 단말에 설치하고 테스트합니다. 모바일 단말에서 “Test Micro App”을 이용해서 테스트합니다. 아직 커스텀 action을 눌러도 아무런 동작을 하지 않습니다. 다음 스텝에서 처리하도록 하겠습니다.

Step 7 - 웨어 앱에서 모바일 단말의 action을 실행하기

Tip:필요하다면 다음의 명령으로 이 스텝을 할 수 있게 바로 준비 할 수 있습니다.

git checkout step7_start

웨어 단말에서 동작하는 앱은 모바일 단말의 activity를 실행할 수 있습니다. MessageApi커스텀 메시지를 보내 모바일 단말에 어떤 일을 하게 하는 방법으로 가능 합니다.

  1. wear/.../service/UtilityService.java 파일에서 startDeviceActivity() instance method 를 찾습니다. 조심하세요, static과 instance 두 종류의 같은 이름의 method가 있습니다. 이 method는 전 스텝에서 AttractionsGridPagerAdapter 에서 설정한 action들에 의해서 실행됩니다.

  1. TODO를 찾아서 웨어 네트워크의 모든 노드들에서 같은 메시지를 보내도록 반복합니다. 데이터 패스와 extraInfo라고 불리는 추가적인 임의의 데이터가 이 method에서 제공됩니다. Step 6.4와 6.5에서 패스와 extra data가 제공되었습니다.

// TODO: Send message asking devices to start a local activity

Iterator<String> itr = Utils.getNodes(googleApiClient).iterator();

while (itr.hasNext()) {

    // Loop through all connected nodes

    Wearable.MessageApi.sendMessage(

            googleApiClient, itr.next(), path, extraInfo.getBytes());

}

  1. 이제 ‘mobile’ 프로젝트를 열어 mobile/.../service/ListenerService.java 파일에서 onMessageReceived() method 를 찾습니다. 여기에서 모바일 앱으로 보내온 메시지를 받을 수 있습니다. START_ATTRACTION_PATH의 if 문 안에 아래의 내용을 추가하세요.

} else if (Constants.START_ATTRACTION_PATH.equals(messageEvent.getPath())) {

    // TODO: Start DetailActivity activity to show a specific tourist attraction

    // Request for this device open the attraction detail screen

    // to a specific tourist attraction

    String attractionName = new String(messageEvent.getData());

    Intent intent = DetailActivity.getLaunchIntent(this, attractionName);

    intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

    startActivity(intent);

} else if (Constants.START_NAVIGATION_PATH.equals(messageEvent.getPath())) {

  1. START_NAVIGATION_PATH 에 “navigate by walking” action을 처리하기 위해 아래의 내용을 추가합니다.

// TODO: Start maps navigation activity

// Request for this device to start Maps walking navigation to

// specific tourist attraction

String attractionQuery = new String(messageEvent.getData());

Uri uri = Uri.parse(Constants.MAPS_NAVIGATION_INTENT_URI + Uri.encode(attractionQuery));

Intent intent = new Intent(Intent.ACTION_VIEW, uri);

intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

startActivity(intent);

  1. 지금까지 작업한 내용을 웨어와 모바일 단말에 설치하고 테스트합니다. “Test Micro App” 을 이용해서 여행지를 표시하고 커스텀 웨어 action들이 잘 동작하는 지 확인 해주세요.

Step 8 - Notification 끄기 동기화

Tip: 필요하다면 다음의 명령으로 이 스텝을 할 수 있게 바로 준비 할 수 있습니다.

git checkout step8_start

이제 모바일 단말과 웨어 단말에서 각각 Notification을 만들어 보여주고 끌수도 있게 되었습니다. 사용자들이 더 편하게 사용하기 위해서는 notification 끄는 부분이 양 단말에서 MessageApi 를 이용해서 동기화 되어야 합니다.

  1. 웨어  프로젝트의 wear/.../service/UtilityService.java 파일과 모바일 프로젝트의 mobile/.../service/UtilityService.java 파일을 열고 clearRemoteNotifications() methods 를 양 파일에서 찾아 양쪽에 같은 내용을 추가합니다. 웨어 네트워크 노드들을 순회하면서 CLEAR_NOTIFICATIONS_PATH를 이용해 메시지를 보냅니다.

if (connectionResult.isSuccess() && googleApiClient.isConnected()) {

    // TODO: Send message asking devices to clear their notifications

    // Loop through all nodes and send a clear notification message

    Iterator<String> itr = Utils.getNodes(googleApiClient).iterator();

    while (itr.hasNext()) {

        Wearable.MessageApi.sendMessage(

                googleApiClient, itr.next(), Constants.CLEAR_NOTIFICATIONS_PATH, null);

    }

}

  1. mobile/.../service/UtilityService.java 파일에서 showNotification() method 안의 NotificationCompat.Builder를 이용해서 notification을 생성한 부분을 찾습니다. 새 PendingIntent만들어 원격으로 notification들을 지우는 메시지를 보내고 그것을 setDeleteIntent()이용해 builder에 추가합니다.

// The intent to trigger when the notification is dismissed, in this case

// we want to clear remote notifications as well

PendingIntent deletePendingIntent =

        PendingIntent.getService(this, 0, getClearRemoteNotificationsIntent(this), 0);

// Construct the main notification

NotificationCompat.Builder builder = new NotificationCompat.Builder(this)

        ...

        .setDeleteIntent(deletePendingIntent)

  1. wear/.../service/ListenerService.java 파일의 onMessageReceived() method 에서 비슷한 일을 합니다.

PendingIntent deletePendingIntent = PendingIntent.getService(

        this, 0, UtilityService.getClearRemoteNotificationsIntent(this), 0);

Notification notification = new Notification.Builder(this)

        ...

        .setDeleteIntent(deletePendingIntent)

        .build();

  1. 작업내용을 모바일, 웨어 단말에 설치하고 notification을 한 쪽에서 끄면 다른 쪽에서도 꺼지는지 확인 합니다.

Step 9 - 사각형과 원형 화면 최적화

Tip: 필요하다면 다음의 명령으로 이 스텝을 할 수 있게 바로 준비 할 수 있습니다.

git checkout step9_start

모든 웨어 앱은 사각형과 원형 단말에서 테스트가 되어야 합니다. 원형 단말에서 테스트를 하면 현재 작업된 내용으로는 몇가지 레이아웃과 관련된 문제를 발견할 수 있습니다. 이 것을 알아 보도록 하겠습니다.

  1. wear/.../ui/AttractionsActivity.java 파일에서 onCreate() method를 찾습니다.

  1. TODO를 찾아 OnApplyWindowInsetsListener를 GridViewPager에 추가합니다 이것으로 단말의 inset 정보를 받아 AttractionsGridPagerAdapter에 넘겨 줄 수 있습니다. 그리고 pager의 컬럼과 로우 사이에 좀더 마진을 넣어 원형 단말에서 보기 좋게 바꾸겠습니다.

// TODO: Set an OnApplyWindowInsetsListener here

mGridViewPager.setOnApplyWindowInsetsListener(new View.OnApplyWindowInsetsListener() {

    @Override

    public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) {

        final boolean round = insets.isRound();

        // Store system window insets regardless of screen shape

        mInsets.set(insets.getSystemWindowInsetLeft(),

                insets.getSystemWindowInsetTop(),

                insets.getSystemWindowInsetRight(),

                insets.getSystemWindowInsetBottom());

        if (round) {

            // On a round screen calculate the square inset to use.

            // Alternatively could use BoxInsetLayout although that

            // currently has some issues working correctly with GridViewPager

            mInsets = Utils.calculateSquareInsetsOnRoundDevice(

                    getWindowManager().getDefaultDisplay(), mInsets);

            // Set some margins between the row and column pages

            int rowMargin = getResources().getDimensionPixelOffset(R.dimen.page_row_margin);

            int colMargin = getResources().getDimensionPixelOffset(

                    R.dimen.page_column_margin);

            mGridViewPager.setPageMargins(rowMargin, colMargin);

        }

        return insets;

    }

});

  1. wear/.../ui/AttractionsGridPagerAdapter.java 파일의 instantiateItem() method를 찾습니다. Step 6 에서 남겨둔 TODO들이 좀 있을 겁니다. 먼저 화면 inset 정보를 margin으로 추가 하여 컬럼 1, 2를 위한 layout 파라메터들을 좀 조정합니다.

if (column == 0 || column == 1) {

    ...

    // TODO: Adjust padding to account for different screen shapes (Step 9)

    FrameLayout.LayoutParams params =

            (FrameLayout.LayoutParams) textView.getLayoutParams();

    params.bottomMargin = mInsets.bottom;

    params.leftMargin = mInsets.left;

    params.rightMargin = mInsets.right;

    textView.setLayoutParams(params);

    ...

} else if (column == 2) {

  1. 그리고 두개의 action 컬럼을 위해서 밑쪽에 약간의 padding을 추가합니다.

} else if (column == 3) {

    ...

    // TODO: Adjust padding to account for different screen shapes (Step 9)

    textView.setPadding(0, 0, 0, mInsets.bottom);

    ...

} else {

    ...

    // TODO: Adjust padding to account for different screen shapes (Step 9)

    textView.setPadding(0, 0, 0, mInsets.bottom);

    ...

}

  1. 이제 작업된 내용을 단말에 설치해서 layout 조정이 잘 되었는지 확인 합니다. 아래와 같이 보이면 됩니다.

Step 10 - 완료하셨습니다!

축하합니다! 이것으로 코드랩을 완료 하셨습니다.

Tip: 필요하다면 다음의 명령으로 이 스텝을 할 수 있게 바로 준비 할 수 있습니다.

git checkout final

좀더 앱을 개선하는데 도전을 하고 싶다면 아래와 같은 부분을 추가로 더 하실 수 있습니다.

  1. “Navigate”와 “Open on device” action은 유저에게 피드백을 추가로 주어 action을 선택했음을 알게 하여야 합니다. 시스템에서 제공되는 애니메이션들을 사용 할 수 있습니다.

R.drawable.go_to_phone_animation

R.drawable.confirmation_animation

        Tip: wearable support library의 ConfirmationActivity 를 이용하거나 당신 만의 애니메이션을 사용할 수도 있습니다 (stackoverflow post 을 확인하시면 AnimationDrawable에서 애니메이션이 종료했는지 알 수 있습니다).

  1. GridViewPager는 얼마나 많은 로우와 컬럼을 페이지들이 가지고 있는지 알려줘야 합니다. indicator를 추가해서 어떤 page에 있는지 유저에게 알려주세요. 시스템에서 제공하는 indicator를 참고로 한번 확인해보시기 바랍니다.

  1. GridViewPager의 카드뷰 페이지는 카드위의 텍스트가 너무 길면 잘 스크롤 되지 않습니다. 잘 스크롤 되게 개선하세요. R.layout.gridpager_card 와 AttractionsGridPagerAdapter 클래스를 살펴보실 필요가 있습니다.