2016년 8월 4일 목요일

Android 이론 - Intent로 값을 넘길 때 'call by reference'를 실현 하려면??


기본적으로 Intent의 getExtra / putExtr 를 활용하게 되면 call by value 로 적용된다.

Service와 Activity / 프로세스와 프로세스 끼리 객체를 공유하기 위해선 어떻게 할까?

바로 Parcelable 인터페이스를 활용하면 된다.

공유하고자 하는 객체의 클래스 선언 시 Parcelable을 implement 해서 사용하면
Intent의 getParcelable / putParcelable을 사용할 수가 있고 이를 통해서 객체 공유가 가능해진다.

서울 종로 - 석탄회관 소공동뚝배기


뚝배기 집에서 불맛 직화 제육볶음 오징어볶음










Android Studio - LBS(Location Based Service) 프로그래밍 - 2

이번 포스팅은 Google Map을 활용한 위치기반 서비스 앱 개발이다.

이전 포스팅에서 확인하였듯이  LBS 관련 서비스를 제공하기 위해선 Google Play 서비스 활용이 필수적이다.

[현재의 위치값 가져오기 복습]

Fused Location을 이용해서 위치정보를 받아오는 정보는 아래 3가지 방식을 통해서 받아온다.
 - GPS
 - 통신사 NW
 - Wifi NW

위의 세가지 방식 중 GPS가 가장 정확하지만 배터리 소모가 많고 음영지역이 많다는 단점이 있다. 통신사 NW의 경우 상대적으로 음영지역이 적은 반면에 오차율이 1Km ~ 1.5Km로 너무 넓다는 단점이 있다. Wifi 네트워크를 이용하는 방식은 아직 우리나라에서는 법적으로 제한이 걸려있기 때문에 사용할 수 없다.

Fused Location API를 사용하기 전에 설정을 하면 위의 세가지 방법 중 가장 적절한 방법으로 현재의 위치 값을 가져오게 된다.


[Google Map을 활용]

Web 프로그래밍을할 때 Google Map을 이용하는방법은 정말 간단하다.
<script src=~~>를 통해 Google Map API를 임포트하고 지도를 표시 할 Layout을 설정하고 함수 하나만 콜 하면 될 일이었다.

그렇게 간단하던 Google Map이 안드로이드에서는 상대적으로 엄청 복잡한 과정을 거친다.

Manifest 설정, Fragment를 이용한 화면단 소스코딩 게다가 Android Developer Console에 프로젝트를 등록하여 Authentication Key 값을 생성 및 얻어와야지 비로소 사용할 수 있게된다.


[구현하기]

일단 manifest 파일에 permission 세팅

<permission 
    android:name="com.example.ch5_lbs.permission.MAPS_RECEIVE"
    android:protectionLevel="signature"/>
<uses-permission 
    android:name="com.example.ch5_lbs.permission.MAPS_RECEIVE"/>

<uses-permission 
    android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission 
    android:name="android.permission.INTERNET"/>
<uses-permission 
    android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission 
    android:name="com.google.android.providers.gsf.permission.READ_GSERVICES"/>
<uses-permission 
    android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission 
    android:name="android.permission.ACCESS_COARSE_LOCATION"/>

Google Map 사용을 하기 위해서 키값 입력

<meta-data 
    android:name="com.google.android.maps.v2.API_KEY"
    android:value="AIzaSyDQQICJSD3Rx3m6b0AFXiHJOjEeGWM3Sqw"/>
<meta-data 
    android:name="com.google.android.gms.version"
    android:value="@integer/google_play_services_version"/>
<activity android:name=".MainActivity">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>

Google Map을 표현하기 위해서 Layout.xml에 Fragment를 추가

<fragment
    android:id="@+id/map_view"
    android:layout_width="match_parent"
    android:layout_height="0dp"
    android:layout_weight="1"
    android:name="com.google.android.gms.maps.SupportMapFragment"/>

MainActivity.java

package com.example.ch5_lbs;

import android.location.Location;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;

import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.api.GoogleApiClient;
import com.google.android.gms.location.FusedLocationProviderApi;
import com.google.android.gms.location.LocationServices;
import com.google.android.gms.maps.CameraUpdateFactory;
import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.OnMapReadyCallback;
import com.google.android.gms.maps.SupportMapFragment;
import com.google.android.gms.maps.UiSettings;
import com.google.android.gms.maps.model.BitmapDescriptorFactory;
import com.google.android.gms.maps.model.CameraPosition;
import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.Marker;
import com.google.android.gms.maps.model.MarkerOptions;

import java.text.DecimalFormat;
import java.text.SimpleDateFormat;

/** * OnMapReadyCallback: 지도가 준비된 시점을 잡기위한 callback * 이 시점에 map을 이용하지 않으면 시간차의 문제로 nullpoint등의 에러 발생 * */
public class MainActivity extends AppCompatActivity
        implements GoogleApiClient.ConnectionCallbacks,
                    GoogleApiClient.OnConnectionFailedListener, OnMapReadyCallback {

    TextView providerTextView;
    ImageView onOffImageView;
    TextView timeTextView;
    TextView locationTextView;
    TextView accuracyTextView;

    GoogleApiClient googleApiClient;
    FusedLocationProviderApi fusedLocationProviderApi;
    Location currentlocation;

    GoogleMap map;  // fragment에 지도 뿌리는 view
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);

        providerTextView = (TextView) findViewById(R.id.txt_location_provider);
        onOffImageView = (ImageView) findViewById(R.id.img_location_on_off);
        timeTextView = (TextView) findViewById(R.id.gps_time);
        locationTextView = (TextView) findViewById(R.id.gps_location);
        accuracyTextView = (TextView) findViewById(R.id.gps_accuracy);

        /**         * Fused API는 실제 google play service라이브러리에서 제공되는 기능으로         * google의 다양한 service를 활용하기 위한 라이브러리이다.         * 그중 locationservice쪽을 이용하겠다고 선언         */        googleApiClient = new GoogleApiClient.Builder(this)
                .addApi(LocationServices.API)
                .addConnectionCallbacks(this)
                .addOnConnectionFailedListener(this)
                .build();

        fusedLocationProviderApi = LocationServices.FusedLocationApi;
    }

    private void toast(String msg){
        Toast t=Toast.makeText(this, msg, Toast.LENGTH_SHORT);
        t.show();
    }

    private String getDateTime(long time) {
        if (time == 0)
            return "";

        SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

        return formatter.format(new java.util.Date(time));
    }

    private String convertDouble(double input) {
        DecimalFormat format = new DecimalFormat(".######");
        return format.format(input);
    }

    /**     * 위치정보 획득후 호출     * 매개변수의 정보대로 다양한 정보 추출하고 화면 출력 역할     */    private void updateInfo(Location location){
        onOffImageView.setImageResource(R.drawable.on);
        timeTextView.setText(getDateTime(location.getTime()));
        locationTextView.setText("LAT:" + convertDouble(location.getLatitude())
                                +" LNG:" + convertDouble(location.getLongitude()));
        accuracyTextView.setText(location.getAccuracy() + " meters");
    }

    /**     * 위치값 획득후에 호출되어 지도제어     */    private void showMap(Location location){
        // 지도에서의 위치는 LatLng로 표현        LatLng latLng = new LatLng(location.getLatitude(), location.getLongitude());
        // 지도 옵션        CameraPosition position = new CameraPosition.Builder().target(latLng).zoom(16f).build();
        // 지도의 center 위치 이동        map.moveCamera(CameraUpdateFactory.newCameraPosition(position));
        // market 올리기        map.clear();    // 이전 marker지우고 안지우면 중복으로 나옴        map.addMarker(new MarkerOptions().position(latLng).title("Location")
                    .icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_BLUE)));
    }

    @Override    protected void onResume() {
        super.onResume();
        googleApiClient.connect();  // 겨리과는 callback함수 호출로        if(map==null){
            ((SupportMapFragment)getSupportFragmentManager().findFragmentById(R.id.map_view)).getMapAsync(this);
        }
    }



    @Override
    protected void onPause() {
        super.onPause();
        if(googleApiClient.isConnected()){
            googleApiClient.disconnect();
        }
    }

    @Override
    public void onConnected(@Nullable Bundle bundle) {
        // 위치값 획득        Location location = fusedLocationProviderApi.getLastLocation(googleApiClient);
        if(location != null){
            currentlocation = location;
            updateInfo(location);
            showMap(location);
        }
    }

    @Override
    public void onConnectionSuspended(int i) {
        toast("onConnectionSuspended");
    }

    @Override
    public void onConnectionFailed(@NonNull ConnectionResult connectionResult) {
        toast("onConnectionFailed");
    }

    @Override
    public void onMapReady(GoogleMap googleMap) {
        map = googleMap;
        UiSettings settings = map.getUiSettings();
        settings.setZoomControlsEnabled(true);

        map.setOnInfoWindowClickListener(new GoogleMap.OnInfoWindowClickListener(){
            @Override
            public void onInfoWindowClick(Marker marker) {
                toast("info window click...");
            }
        });
    }
}



Android 이론 - 안드로이드 문자열 타입


기본적인 자바의 문자열 타입은 두말할 나위 없이 String이다.
그런데 안드로이드에서는 String의 상위 타입으로 Charsequence 타입을 두었다.

그 이유는 무엇일까

CharSequence 밑에 String과 같은 티어로 Spannable 클래스를 만들어 제공하기 위해서

그럼 Spannable은 String과 무엇이 다르기때문에??

1. String은 오로지 Data만 담고 있다.
2. Spannable은 Data + Design 까지 담을 수가 있다.


Hello World 라는 표현을 하고자할 때 기존 String을 이용하려면 View에서 복잡하게 스타일을 적용해 주어야 한다.

Spannable 에서는 setSpan 함수를 이용해 자유자재로 스타일을 적용해 줄 수가 있다.
게다가 <b></b> <br/> 등의 html 태그 까지 알아서 적용해주는 기능까지 제공한다. (그렇다고 css style까지 해준다는건 아님)

Android 이론 - Permission 정리 (계속)


Permission 관련 태그 정리

1. <uses-permission>
2. <permission>



<permission>
android:name="AAA"로 정의 가능하고 AAA라는 이름은 유일성이 보장되어야 함
사용자 정의 permission 정도 됨
android:protectionLevel로 접근레벨 설정이 가능하고 주로 'normal/dangerous/signature'
signature 는 <uses-permission> 선언만으로 사용이 불가능하고, 제공 벤더와의 key 값이 일치해야 사용이 가능


Android Studio - LBS(Location Based Service) 프로그래밍 - 1



[위치정보 가져오기]

위치정보는 Android 위치정보 표준 API를 이용하는 방법이 있고, Fused Location을 이용하는 방법이 있다.
표준 API는 다양한 Location Provider를 자유롭게 이용할 수 있는 반면에, 개발하는 동안에 고려해서 처리해야 하는 부분이 너무 많다.
그래서 보다 편리한 개발을 위해서 활용할 수 있는 Fused Location을 이용하자.

Fused Location은 Google Play 라이브러리를 Dependency에 추가해야지 사용할 수 있다.

Graddle 에 dependency 추가
프로젝트 익스플로러에서 '모듈 우클릭' -> 'open module setting' -> 'dependency 탭 선택' -> '우측의 + 버튼 클릭'

com.google.android.gms:play-services:9.2.1 추가

Graddle에 추가된 것 확인

dependencies {
    compile fileTree(include: ['*.jar'], dir: 'libs')
    testCompile 'junit:junit:4.12'    compile 'com.android.support:appcompat-v7:23.4.0'    compile 'com.google.android.gms:play-services:9.2.1'}


MainActivity.java

package com.example.ch5_lbs;

import android.location.Location;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;

import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.api.GoogleApiClient;
import com.google.android.gms.location.FusedLocationProviderApi;
import com.google.android.gms.location.LocationServices;

import java.text.DecimalFormat;
import java.text.SimpleDateFormat;


public class MainActivity extends AppCompatActivity        implements GoogleApiClient.ConnectionCallbacks,
                    GoogleApiClient.OnConnectionFailedListener{

    TextView providerTextView;
    ImageView onOffImageView;
    TextView timeTextView;
    TextView locationTextView;
    TextView accuracyTextView;

    GoogleApiClient googleApiClient;
    FusedLocationProviderApi fusedLocationProviderApi;
    Location currentlocation;

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

        setContentView(R.layout.activity_main);

        providerTextView = (TextView) findViewById(R.id.txt_location_provider);
        onOffImageView = (ImageView) findViewById(R.id.img_location_on_off);
        timeTextView = (TextView) findViewById(R.id.gps_time);
        locationTextView = (TextView) findViewById(R.id.gps_location);
        accuracyTextView = (TextView) findViewById(R.id.gps_accuracy);

        /**         * Fused API는 실제 google play service라이브러리에서 제공되는 기능으로         * google의 다양한 service를 활용하기 위한 라이브러리이다.         * 그중 locationservice쪽을 이용하겠다고 선언         */        googleApiClient = new GoogleApiClient.Builder(this)
                .addApi(LocationServices.API)
                .addConnectionCallbacks(this)
                .addOnConnectionFailedListener(this)
                .build();

        fusedLocationProviderApi = LocationServices.FusedLocationApi;
    }

    private void toast(String msg){
        Toast t=Toast.makeText(this, msg, Toast.LENGTH_SHORT);
        t.show();
    }

    private String getDateTime(long time) {
        if (time == 0)
            return "";

        SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

        return formatter.format(new java.util.Date(time));
    }

    private String convertDouble(double input) {
        DecimalFormat format = new DecimalFormat(".######");
        return format.format(input);
    }

    /**     * 위치정보 획득후 호출     * 매개변수의 정보대로 다양한 정보 추출하고 화면 출력 역할     */    private void updateInfo(Location location){
        onOffImageView.setImageResource(R.drawable.on);
        timeTextView.setText(getDateTime(location.getTime()));
        locationTextView.setText("LAT:" + convertDouble(location.getLatitude())
                                +" LNG:" + convertDouble(location.getLongitude()));
        accuracyTextView.setText(location.getAccuracy() + " meters");
    }

    @Override    protected void onResume() {
        super.onResume();
        googleApiClient.connect();  // 겨리과는 callback함수 호출로    }

    @Override    protected void onPause() {
        super.onPause();
        if(googleApiClient.isConnected()){
            googleApiClient.disconnect();
        }
    }

    @Override    public void onConnected(@Nullable Bundle bundle) {
        // 위치값 획득        Location location = fusedLocationProviderApi.getLastLocation(googleApiClient);
        if(location != null){
            currentlocation = location;
            updateInfo(location);
        }
    }

    @Override    public void onConnectionSuspended(int i) {
        toast("onConnectionSuspended");
    }

    @Override    public void onConnectionFailed(@NonNull ConnectionResult connectionResult) {
        toast("onConnectionFailed");
    }
}


2016년 8월 3일 수요일

Android Studio - 내장 Camera를 연동하는 간단한 카메라 어플

카메라를 활용하는 방법은 2가지 이다.

- 기본 카메라 어플의 Activity를 활용해서 사진을 찍고 파일 처리
- Camera API를 이용하여 직접 내장카메라를 조작

1. Android manifest permission 수정
2. Camera API 이용
3. SurfaceView

[Android manifest permission 수정]
카메라 활용을 위해서 permission 태그를 추가한다.

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

<uses-permission/>은 알겠는데 <uses-feature/>는 또 처음 등장하는 개념이다.
feature는 해당 기능에 대한 H/W 적인 지원이 가능한지에 대한 여부를 묻는다고 보면 된다.
어차피 지금 만들어지는 스마트폰에서 카메라가 없을리는 만무하니까 해당 태그는 그냥 지워버리자.

[Camera API 이용] Camera API 는 제조사들의 카메라 성능 향상으로 인해서 롤리팝(5.0) 이상의 안드로이드부터는 기존 API와는 전혀 별개의 Camera API2가 새로이 만들어져서 제공되고 있다.

하지만 해당 기능을 모두 활용하기 위해선 터무니 없이 높아진 추상화 레벨에 대한 이해가 필요하고, 단순한 사진찍기 기능 구현에도 엄청난 양의 소스코드가 추가되므로,
Camera API 를 활용해서 앱을 만들어 보도록 한다.

[SurfaceView] 
애니메이션 화면 표현에서 가장 필수적인 기술요소가 있다면 Double Buffering일 것이다.

사용자의 눈을 통해서 보여지는 Display가 보여줄 화면을 완성하기도 전에 그려지고 있는 모습을 뿌려준다면 사용자는 애니메이션이 매끄럽지 못하다고 느낄 것이다.

이를 해결하기 위해서 그리기가 완성되어 사용자의 눈에 보여지는 View 외에, 다음번에 보여질 이미지를 그리기 위한 View를 메모리에 올려서 처리하게 된다.

이렇게 뒤에서 이미지를 그려주는 기술을 Double Buffering 이라고 하는데 안드로이드 Camera 에서는 SurfaceView라는 클래스가 이것을 지원 해 준다.

SurfaceView는 SurfaceHolder와 SurfaceHolder.Collback 클래스를 이용하여 Double Buffering 처리를 구현해야 하는데, 더욱 멋진 것은 Camera API가 Preview 화면을 이미 그 처리를 완료해서 우리에게 SurfaceView만 넘겨준다는 것이다.

완성된 CameraView 소스코드

package com.example.ch4_camera;

import android.app.Activity;
import android.hardware.Camera;
import android.os.Environment;
import android.util.Log;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;

import java.io.File;
import java.io.FileOutputStream;
import java.util.List;

/** * Created by student on 2016-08-04. */public class MyCameraView extends SurfaceView implements SurfaceHolder.Callback{
    Activity context;
    /**     * surface 작업자     * 일반적으로는 holdersurface점유     * drawing이 되지만 camera class에서는 이미 surface로 그린상태로     * 넘기기 때문에 넘어오는 surface를 받기만 한다     */    SurfaceHolder holder;
    Camera camera;
    List<Camera.Size> supportedPreviewSizes;
    Camera.Size previewSize;

    int width;
    int height;

    public MyCameraView(Activity context){
        super(context);
        this.context = context;

        // surfaceview만 나오면 무조건 나오는 2줄의 소스코드        holder = getHolder();
        holder.addCallback(this);
    }


    // surface가 최초로 준비되는 순간 딱 한번 호출    @Override    public void surfaceCreated(SurfaceHolder holder) {
        // camera 점유        camera = Camera.open();
        // surface에 직접 그리는게 아니라 camera에서부터 넘어오는 것을 그대로 받는것        holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
        try{
            // camera surface holder에게 전달되게 연결            camera.setPreviewDisplay(holder);
            // camera 설정            Camera.Parameters parameters = camera.getParameters();
            /**             * preview 사이즈를 개발자가 임의 숫자로 주면 에러 나기때문에             * camera에서 지원하는 size목록 중 하나를 대입해야 한다.             */            supportedPreviewSizes = parameters.getSupportedPreviewSizes();
            /**             * camera에서 제공하는 preview사이즈를 그대로 대입하는게 아니라             * 폰의 사이즈와 대비해서 계산             */            if(supportedPreviewSizes != null){
                previewSize = getOptimalPreviewSize(supportedPreviewSizes, width, height);
            }
        }catch(Exception e){
            camera.release();
            camera = null;
        }
    }

    /**     * surface공간의 사이즈가 변경될때마다 반복호출     * 일반적으로는 created호출 후 바로 호출된후 다시 호출은 안된다.     */    @Override    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
        Camera.Parameters parameters = camera.getParameters();
        parameters.setPreviewSize(previewSize.width, previewSize.height);
        setCameraDisplayOrientation(context, 0);
        parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);
        camera.setParameters(parameters);
        // preview 출력 시작        camera.startPreview();
    }

    @Override    public void surfaceDestroyed(SurfaceHolder holder) {
        if(camera != null){
            camera.stopPreview();
            camera.release();
            camera = null;
        }
    }

    // 사진 찍기 위한 개발자 함수. activity에서 호출할 것임    public void capture(){
        if(camera != null){
            camera.takePicture(null, null, new Camera.PictureCallback(){
                @Override                public void onPictureTaken(byte[] data, Camera camera) {
                    // file write                    FileOutputStream fos;
                    try{
                        File dir = new File(
                                Environment.getExternalStorageDirectory().getAbsolutePath() + "/multi");
                        if(!dir.exists()){
                            dir.mkdir();
                        }
                        // createTempFile( 파일명 prefix, 파일 확장자, 파일 저장폴더)                        File file = File.createTempFile("mulcam", ".jpg", dir);
                        if(!file.exists()){
                            file.createNewFile();
                        }
                        fos = new FileOutputStream(file);
                        fos.write(data);
                        fos.flush();
                        fos.close();
                        Log.d("mulcam", file.getAbsolutePath());
                    }catch (Exception e){
                        e.printStackTrace();
                    }
                    // 사진을 찍고 나면 찍은이미지에서 화면이 멈춰있기때문에 연속적인 촬영을 위해                    camera.startPreview();
                }
            });
        }
    }

    private Camera.Size getOptimalPreviewSize(List<Camera.Size> sizes, int w, int h) {
        final double ASPECT_TOLERANCE = 0.1;
        double targetRatio=(double)h / w;

        if (sizes == null) return null;

        Camera.Size optimalSize = null;
        double minDiff = Double.MAX_VALUE;

        int targetHeight = h;

        for (Camera.Size size : sizes) {
            double ratio = (double) size.width / size.height;
            if (Math.abs(ratio - targetRatio) > ASPECT_TOLERANCE) continue;
            if (Math.abs(size.height - targetHeight) < minDiff) {
                optimalSize = size;
                minDiff = Math.abs(size.height - targetHeight);
            }
        }

        if (optimalSize == null) {
            minDiff = Double.MAX_VALUE;
            for (Camera.Size size : sizes) {
                if (Math.abs(size.height - targetHeight) < minDiff) {
                    optimalSize = size;
                    minDiff = Math.abs(size.height - targetHeight);
                }
            }
        }
        return optimalSize;
    }

    private void setCameraDisplayOrientation(Activity activity, int cameraId)
    {
        Camera.CameraInfo info = new Camera.CameraInfo();
        Camera.getCameraInfo(cameraId, info);
        int rotation = activity.getWindowManager().getDefaultDisplay().getRotation();
        int degrees = 0;
        switch (rotation)
        {
            case Surface.ROTATION_0:
                degrees = 0;
                break;
            case Surface.ROTATION_90:
                degrees = 90;
                break;
            case Surface.ROTATION_180:
                degrees = 180;
                break;
            case Surface.ROTATION_270:
                degrees = 270;
                break;
        }

        int result;
        if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT)
        {
            result = (info.orientation + degrees) % 360;
            result = (360 - result) % 360; // compensate the mirror        }
        else        { // back-facing            result = (info.orientation - degrees + 360) % 360;
        }
        camera.setDisplayOrientation(result);
    }

    @Override    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        width = resolveSize(getSuggestedMinimumWidth(), widthMeasureSpec);
        height = resolveSize(getSuggestedMinimumHeight(), heightMeasureSpec);
        setMeasuredDimension(width, height);
    }
}