레이블이 안드로이드인 게시물을 표시합니다. 모든 게시물 표시
레이블이 안드로이드인 게시물을 표시합니다. 모든 게시물 표시

2019년 3월 13일 수요일

안드로이드 PIP 예제 - Picture In Picture

- PIP 지원
https://developer.android.com/guide/topics/ui/picture-in-picture?hl=ko
https://developer.android.com/about/versions/oreo/android-8.0?hl=ko#opip



1. 기본구성
- AndroidManifest.xml
<activity
android:name=".MainActivity"
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
android:resizeableActivity="true"
android:supportsPictureInPicture="true">


- 미디어 플레이어 코드가 구현된 상태에서 Actvity 추가사항들.
PictureInPictureParams.Builder pictureInPictureParamsBuilder = new PictureInPictureParams.Builder();
Rational aspectRatio = new Rational(mediaPlayer.getVideoWidth(), mediaPlayer.getVideoHeight());
PictureInPictureParams build = pictureInPictureParamsBuilder.setAspectRatio(aspectRatio).build();
enterPictureInPictureMode(build);

@Override
public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode) {
super.onPictureInPictureModeChanged(isInPictureInPictureMode);
Log.d(TAG, "onPictureInPictureModeChanged : " + isInPictureInPictureMode);
}


> PIP 동작 상태가 변경되면 onPictureInPictureModeChanged 메소드가 호출됩니다.
> enterPictureInPictureMode 메소드를 호출하면 PIP 모드로 동작합니다.


- Activity 전체코드
public class MainActivity extends AppCompatActivity {

    private static final String TAG = MainActivity.class.getSimpleName();
    private String mediaURL = "https://sample-videos.com/video123/mp4/720/big_buck_bunny_720p_20mb.mp4";
    private MediaPlayer mediaPlayer;
    private boolean minimize;

    private final Object pictureInPictureParamsBuilder;
    {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            pictureInPictureParamsBuilder = new PictureInPictureParams.Builder();
        } else {
            pictureInPictureParamsBuilder = null;
        }
    }

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

        setContentView(R.layout.activity_main);


        SurfaceView surfaceView = findViewById(R.id.surfaceView);
        surfaceView.getHolder().addCallback(new SurfaceHolder.Callback() {
            @Override
            public void surfaceCreated(SurfaceHolder holder) {
                initMediaPlayer(holder);
            }

            @Override
            public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {

            }

            @Override
            public void surfaceDestroyed(SurfaceHolder holder) {

            }
        });

        View actionPIP = findViewById(R.id.actionPIP);
        actionPIP.setVisibility(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ? View.VISIBLE : View.GONE);
        actionPIP.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                minimize();
            }
        });
    }

    private void initMediaPlayer(SurfaceHolder holder) {
        holder.getSurface();
        mediaPlayer = new MediaPlayer();
        mediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
            @Override
            public void onPrepared(MediaPlayer mp) {
                mp.start();
            }
        });
        mediaPlayer.setSurface(holder.getSurface());

        try {
            mediaPlayer.setDataSource(getBaseContext(), Uri.parse(mediaURL));
            mediaPlayer.prepareAsync();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private void play() {
        if (mediaPlayer != null) {
            if (mediaPlayer.isPlaying() == false) {
                mediaPlayer.start();
            }
        }
    }

    @Override
    protected void onResume() {
        super.onResume();
        play();
    }

    @Override
    protected void onPause() {
        super.onPause();
        if (minimize == false)
            if (mediaPlayer != null) {
                mediaPlayer.pause();
            }
    }

    @Override
    protected void onDestroy() {
        if (mediaPlayer != null) {
            mediaPlayer.release();
        }
        super.onDestroy();
    }


    /**
     * PIP 모드
     */
    private void minimize() {
        if (mediaPlayer == null) {
            return;
        }
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            if (pictureInPictureParamsBuilder instanceof PictureInPictureParams.Builder) {
                Rational aspectRatio = new Rational(mediaPlayer.getVideoWidth(), mediaPlayer.getVideoHeight());
                PictureInPictureParams build = ((PictureInPictureParams.Builder) pictureInPictureParamsBuilder).setAspectRatio(aspectRatio).build();
                minimize = true;
                enterPictureInPictureMode(build);
            }
        }
    }


    @Override
    public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode) {
        super.onPictureInPictureModeChanged(isInPictureInPictureMode);
        Log.d(TAG, "onPictureInPictureModeChanged : " + isInPictureInPictureMode);
        if (isInPictureInPictureMode) {
            play();
        } else {
            minimize = false;
        }
    }
}



3. 샘플코드
소스코드
APK

  


샘플 mp4 영상
https://sample-videos.com/video123/mp4/720/big_buck_bunny_720p_20mb.mp4

구글 샘플코드
https://github.com/googlesamples/android-PictureInPicture/#readme

FCM 클라이언트 예제

1. FCM 환경구성
- 파이어베이스 프로젝트 생성 및 어플리케이션 등록
https://console.firebase.google.com/

> keytool을 이용해서 어플리케이션을 파이어베이스 프로젝트와 연동(인증)
> keytool의 위치는 JAVA SDK의 bin 디렉토리에 있음.
https://developers.google.com/android/guides/client-auth

> google-services.json 파일은 [프로젝트]/app 디렉토리에 포함시킨다.



> 프로젝트 연동을 확인 하면 기본 설정은 완료.



2. 기본구성
- FCM gradle 구성
https://firebase.google.com/docs/cloud-messaging/android/client?hl=ko


- FirebaseMessagingService 상속
public class MyFirebaseMessagingService extends FirebaseMessagingService{}


- FCM 서비스 등록 - AndroidManifest.xml
<service android:name=".MyFirebaseMessagingService">
    <intent-filter>
        <action android:name="com.google.firebase.MESSAGING_EVENT" />
    </intent-filter>
</service>

> MyFirebaseMessagingService는 FirebaseMessagingService를 상속 한다.



2. 기본코드
- MyFirebaseMessagingService
public class MyFirebaseMessagingService extends FirebaseMessagingService {
    private static final String TAG = MyFirebaseMessagingService.class.getSimpleName();

    /*
     * Activity가 onResume 상태가 아닐 때는 Cloud Message에서 전송한 메시지는 수신되지 않는다.
     */
    @Override
    public void onMessageReceived(RemoteMessage remoteMessage) {
        super.onMessageReceived(remoteMessage);
        String from = remoteMessage.getFrom();
        String messageId = remoteMessage.getMessageId();
        String messageType = remoteMessage.getMessageType();

        Map<String, String> map = remoteMessage.getData();
        int size = map.size();
        Log.d(TAG, "RemoteMessage from : " + from);
        Log.d(TAG, "RemoteMessage id : " + messageId);
        Log.d(TAG, "RemoteMessage type : " + messageType);
        Log.d(TAG, "RemoteMessage size : " + size);
        if (size > 0) {
            JSONObject jsonObject = new JSONObject(map);
            Log.d(TAG, "RemoteMessage data : " + jsonObject.toString());
        }

        RemoteMessage.Notification notification = remoteMessage.getNotification();
        if (notification != null) {
            String title = notification.getTitle();
            String body = notification.getBody();
            Log.d(TAG, "RemoteMessage notification title : " + title);
            Log.d(TAG, "RemoteMessage notification body : " + body);
        }
    }

    @Override
    public void onNewToken(String s) {
        super.onNewToken(s);
        Log.d(TAG, "TOKEN : " + s);
    }
}

> 파이어베이스의 Cloud Message는 액티비티 상태에 따라 메시지 수신 결과가 달라진다.
> FCM 메시지의 data 영역만 사용할 때는 항상 MyFirebaseMessagingService 가 수신한다.



- MainActivity
public class MainActivity extends AppCompatActivity {

    private static final String TAG = MainActivity.class.getSimpleName();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Task<InstanceIdResult> id = FirebaseInstanceId.getInstance().getInstanceId();
        id.addOnCompleteListener(new OnCompleteListener<InstanceIdResult>() {
            @Override
            public void onComplete(@NonNull Task<InstanceIdResult> task) {
                // 토큰 확인
                if (task.isSuccessful()) {
                    InstanceIdResult result = task.getResult();
                    String id = result.getId();
                    String token = result.getToken();

                    Log.d(TAG, "Token id : " + id);
                    Log.d(TAG, "Token : " + token);

                    TextView tokenView = findViewById(R.id.token);
                    tokenView.setText(task.getResult().getToken());
                } else {
                    Log.d(TAG, "Token Exception : " + task.getException().toString());
                }
            }
        });
    }
}



- AndroidManifest.xml 코드
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="app.example.fcmapp">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">

        <meta-data
            android:name="com.google.firebase.messaging.default_notification_channel_id"
            android:value="@string/default_notification_channel_id" />

        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

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

        <service android:name=".MyFirebaseMessagingService">
            <intent-filter>
                <action android:name="com.google.firebase.MESSAGING_EVENT" />
            </intent-filter>
        </service>
    </application>
</manifest>



- Cloud Message 전송 테스트
> 백그라운드 또는 알림센터에서 메시지가 수신된다.



3. 샘플코드
소스코드
APK



2019년 3월 12일 화요일

안드로이드 프로젝트 gitignore 리스트

- 프로젝트 복사와 SVN 업로드 시에 불필요한 부분을 덜어내야 관리하기 편리합니다.


1. gitignore 목록
- 프로젝트 루트 디렉토리의 .gitignore 파일은 아래와 같이 설정
*.iml
.gradle/
.idea
/*/build/
/local.properties
/captures
.externalNativeBuild
.DS_Store
Thumbs.db

> app 디렉토리의 .gitignore 파일이 아닌 루트 디렉토리의 .gitignore 파일 입니다.
> 루트 디렉토리의 .gitignore이 없을 경우 새로 생성 하거나 안드로이드 스튜디오에서 직접 gitignore 항목을 지정합니다.




2. 프로젝트 압축 시 용량 줄이기
- 프로젝트 루트 디렉토리





























.gradle 디렉토리 제외
.idea 디렉토리 제외
local.properties 파일 제외
.iml 확장자 제외




- 어플리케이션 디렉토리






























build 디렉토리 제외
.iml 확장자 제외

콜백 인터페이스 - ResultReceiver

- ResultReceiver를 더 자세히 이해하기 위해서는 안드로이드 IPC 자료를 찾아볼 것.
ResultReceiver 클래스는 Parcelable 인터페이스를 구현하므로 Parcelable 객체를 전달 받을 수 있는 모든 곳에서 콜백을 처리할 수 있다.


1. ResultReceiver 기본코드
- ResultReceiver 콜백
ResultReceiver resultReceiver = new ResultReceiver(textView.getHandler()) {
    @Override
    protected void onReceiveResult(int resultCode, Bundle resultData) {
        super.onReceiveResult(resultCode, resultData);
        // 콜백
        if (resultCode == RESULT_OK && resultData != null) {
            String text = resultData.getString("message");
            textView.setText(text);
        }
    }
};



ResultReceiver 콜백 파라미터
BlankFragment fragment = new BlankFragment();
Bundle args = new Bundle();
args.putParcelable(ResultReceiver.class.getSimpleName(), resultReceiver);
fragment.setArguments(args);

또는

Intent intent = new Intent(getBaseContext(), SubActivity.class);
intent.putExtra(ResultReceiver.class.getSimpleName(), resultReceiver);
startActivity(intent);


ResultReceiver가 Parcelable를 구현하므로 Parcelable를 수신할 수 있는 모든 곳에서 콜백을 동작시킬 수 있다.
> 주로 Activity와 Service 간 통신에 사용된다.



- Bundle 전송
ResultReceiver resultReceiver = getArguments().getParcelable(ResultReceiver.class.getSimpleName());
Bundle bundle = new Bundle();
bundle.putString("message", "Hello, World!");
resultReceiver.send(Activity.RESULT_OK, bundle);


> sned를 통해 Bundle를 전송하면 onReceiveResult 메소드에서 수신한다.




2. 샘플코드
소스코드
APK


안드로이드 관리자 권한 예제 - DevicePolicyManager

- 관리자 권한 취득
- 카메라 사용 제한하기
- 앱 삭제 제한하기(어플리케이션 관리 화면에서는 삭제 가능)


1. 관리자 권한
- xml 정의 - device_admin_receiver.xml
<device-admin xmlns:android="http://schemas.android.com/apk/res/android">
    <uses-policies>
        <disable-camera />
    </uses-policies>
</device-admin>

> res 디렉토리의 xml 디렉토리에 xml 문서 생성
> 카메라 제어를 위해 <disable-camera /> 적용
> 레퍼런스 :
https://developer.android.com/guide/topics/admin/device-admin
> 안드로이드 9 버전 이후 부터 변경 사항이 있습니다.
https://developers.google.com/android/work/device-admin-deprecation


- DeviceAdminReceiver  상속
public class AppDeviceAdminReceiver extends DeviceAdminReceiver {
    private static final String TAG = AppDeviceAdminReceiver.class.getSimpleName();

    @Override
    public void onEnabled(Context context, Intent intent) {
        super.onEnabled(context, intent);
        Log.d(TAG, "Admin onEnabled");
    }

    @Override
    public void onDisabled(Context context, Intent intent) {
        super.onDisabled(context, intent);
        Log.d(TAG, "Admin onDisabled");
    }
}


- 리시버 등록 - AndroidManifest.xml
<receiver
android:name=".AppDeviceAdminReceiver"
android:description="@string/device_admin_description"
android:label="@string/app_name"
android:permission="android.permission.BIND_DEVICE_ADMIN">
<meta-data
android:name="android.app.device_admin"
android:resource="@xml/device_admin_receiver" />
<intent-filter>
<action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
</intent-filter>
</receiver>


- 관리자 권한 요청
ComponentName componentName = new ComponentName(this, AppDeviceAdminReceiver.class);
Intent intent = new Intent(DevicePolicyManager.ACTION_ADD_DEVICE_ADMIN);
intent.putExtra(DevicePolicyManager.EXTRA_DEVICE_ADMIN, componentName);
startActivityForResult(intent, DEVICE_ADMIN_ADD_RESULT_ENABLE);


> 권한 확인
final boolean adminActive = devicePolicyManager.isAdminActive(componentName);

> 카메라 제어 확인
boolean cameraDisabled = devicePolicyManager.getCameraDisabled(componentName);




2. 샘플코드
소스코드
APK

 



2019년 3월 8일 금요일

Room 아키텍처 예제

- 아키텍처 구조


- 구글 코드랩 예제 :
https://codelabs.developers.google.com/codelabs/android-room-with-a-view/index.html?index=..%2F..index#0




1. 기본 구성
- dependencies 구성
def room_version = "2.1.0-alpha04"
implementation "androidx.room:room-runtime:$room_version"
annotationProcessor "androidx.room:room-compiler:$room_version"

def lifecycle_version = "2.0.0"
// ViewModel and LiveData
implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"

> 라이프사이클
https://developer.android.com/jetpack/androidx/releases/lifecycle?hl=ko#declaring_dependencies
> Room
https://developer.android.com/jetpack/androidx/releases/room?hl=ko



- Entity
@Entity(tableName = "user_table")
public class User {

    @PrimaryKey(autoGenerate = true)
    @ColumnInfo(name = "id")
    private int id;

    @NonNull
    @ColumnInfo(name = "userName")
    private String userName;

    @NonNull
    @ColumnInfo(name = "age")
    private int age;

}

> autoGenerate를 적용하면 insert 과정에서 id 값이 자동으로 증가한다.



- Dao
@Dao
public interface UserDao {
    @Insert
    long insert(User user);

    @Update
    int update(User user);

    @Query("DELETE FROM user_table")
    int deleteAll();

    @Query("DELETE FROM user_table WHERE id = :id")
    int deleteUser(int id);

    @Query("SELECT * from user_table ORDER BY userName ASC")
    LiveData<List<User>> getAllUsers();

}

> LiveData를 사용하지 않을 경우
    @Query("SELECT * from user_table ORDER BY userName ASC")
    List<User> getAllUsers();



- Database
@Database(entities = {User.class}, version = 1)
public abstract class UserRoomDatabase extends RoomDatabase {
    public abstract UserDao userDao();

    private static volatile UserRoomDatabase INSTANCE;

    static UserRoomDatabase getDatabase(final Context context) {
        if (INSTANCE == null) {
            synchronized (UserRoomDatabase.class) {
                if (INSTANCE == null) {
                    INSTANCE = Room.databaseBuilder(context.getApplicationContext(), UserRoomDatabase.class, "user_database").build();
                }
            }
        }
        return INSTANCE;
    }
}


- 설명
androidx.room:room-compiler가 generatedJava 디렉토리의 소스코드를 자동 생성한다.

SQLiteOpenHelper를 사용했을 때 보다 더 생산성이 좋다.
링크 : 






2. 안드로이드 앱 아키텍처 적용



Room 아키텍처는 안드로이드 앱 아키텍처의 DB 연동 구성요소이다.


- Repository
class UserRepository {
    private static final String TAG = UserRepository.class.getSimpleName();

    private final UserDao userDao;
    private final LiveData<List<User>> allUsers;

    UserRepository(Application application) {
        UserRoomDatabase db = UserRoomDatabase.getDatabase(application);
        userDao = db.userDao();
        allUsers = userDao.getAllUsers();
    }

    public void insert(User user) {
        new AsyncTask<User, Void, Long>() {
            @Override
            protected Long doInBackground(User... users) {
                if (userDao == null)
                    return -1L;
                return userDao.insert(users[0]);
            }

            @Override
            protected void onPostExecute(Long aLong) {
                super.onPostExecute(aLong);
                Log.d(TAG, "insert : " + aLong);
            }
        }.execute(user);
    }

    public void update(User user) {
        new AsyncTask<User, Void, Integer>() {
            @Override
            protected Integer doInBackground(User... users) {
                if (userDao == null)
                    return -1;
                return userDao.update(users[0]);
            }

            @Override
            protected void onPostExecute(Integer integer) {
                super.onPostExecute(integer);
                Log.d(TAG, "update : " + integer);
            }
        }.execute(user);
    }

    public void deleteAll() {
        new AsyncTask<Void, Void, Integer>() {
            @Override
            protected Integer doInBackground(Void... voids) {
                if (userDao == null)
                    return -1;
                return userDao.deleteAll();
            }

            @Override
            protected void onPostExecute(Integer integer) {
                super.onPostExecute(integer);
                Log.d(TAG, "deleteAll : " + integer);
            }
        }.execute();
    }

    public void deleteUser(int id) {
        new AsyncTask<Integer, Void, Integer>() {
            @Override
            protected Integer doInBackground(Integer... integers) {
                if (userDao == null)
                    return -1;
                return userDao.deleteUser(integers[0]);
            }

            @Override
            protected void onPostExecute(Integer integer) {
                super.onPostExecute(integer);
                Log.d(TAG, "deleteUser : " + integer);
            }
        }.execute(id);
    }

    public LiveData<List<User>> getAllUsers() {
        return allUsers;
    }
}

> 메인쓰레드에서 DB 연동을 사용하지 않아야 한다.
> LiveData를 사용하지 않는 곳은 AsyncTask를 적용한다.



- AndroidViewModel 
public class UserViewModel extends AndroidViewModel {
    private static final String TAG = UserViewModel.class.getSimpleName();

    private final UserRepository repository;
    private final LiveData<List<User>> allUsers;

    public UserViewModel(Application application) {
        super(application);
        repository = new UserRepository(application);
        allUsers = repository.getAllUsers();
    }

    public void insert(User user) {
        repository.insert(user);
    }

    public void update(User user) {
        repository.update(user);
    }

    public void deleteAll() {
        repository.deleteAll();
    }

    public void deleteUser(int id) {
        repository.deleteUser(id);
    }

    public LiveData<List<User>> getAllUsers() {
        return allUsers;
    }
}

> AndroidViewModel를 상속하고 대부분의 메소드들은 델리게이트로 구성한다.



- ViewModel 구현
userViewModel = ViewModelProviders.of(this).get(UserViewModel.class);
userViewModel.getAllUsers().observe(this, new Observer<List<User>>() {
@Override
public void onChanged(List<User> users) {

}
});

> 라이프사이클을 이용해서 ViewModel을 통해 DB 연동을 한다.
> 라이프 사이클 :
https://developer.android.com/jetpack/androidx/releases/lifecycle?hl=ko#declaring_dependencies



3. 샘플코드
소스코드
APK




- Room의 구성을 더 강력하게 만드는 구성은 Room + ViewModel + Paging 조합이다.
추가적으로 Data Binding을 함께 구성하기도 하지만 반드시 필요한 경우만 사용해하 한다.

- Room을 사용하는 주요 목적은 안드로이드 앱 아키텍처 구조 적용과 SQLiteOpenHelper의 불편함을 줄이기 위함이다.

- 안드로이드와 SQLite의 관계를 이해하고 Room 아키텍처를 사용하도록 하자.

2019년 3월 4일 월요일

ConstraintLayout 가이드

1. layout_constraint 속성

- app:layout_constraintTop_toTopOf="parent"
parent 속성으로 ConstraintLayout에 배치된 뷰는 최 상단을 기준으로 배치.

간단하게 자석처럼 최상단에 View을 붙는다.

예1 ) 모든 방향을 적용하면
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"


android:layout_width="wrap_content", android:layout_height="wrap_content"
크기로 배치된 View는 레이아웃 가운데 위치하게 된다.





android:layout_width="0dp", android:layout_height="0dp"
크기로 배치된 View는 레이아웃 모든 영역에서 레이아웃을 잡아 당겨지므로 전체화면으로 표시된다.





예2 ) 다른 레이아웃에 붙일 경우
app:layout_constraintTop_toBottomOf="@id/textView"




> 간단하게 Android 텍스트 뷰를 textView 아래 배치 하는 의미.


자주 사용하는 속성은
app:layout_constraintTop_toBottomOf="@id/textView"
app:layout_constraintLeft_toRightOf="@id/textView"
app:layout_constraintRight_toLeftOf="@id/textView"
app:layout_constraintBottom_toTopOf="@id/textView"



- 레이아웃 샘플코드
<?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"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#d1c4e9"
        android:gravity="center"
        android:text="Hello World!"
        android:textSize="30dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />


    <TextView
        android:id="@+id/textView2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#d1c4e9"
        android:gravity="center"
        android:text="Android"
        android:layout_marginTop="8dp"
        android:textSize="30dp"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@id/textView" />

</androidx.constraintlayout.widget.ConstraintLayout>








2. layout_constraintDimensionRatio
레이아웃을 16:9, 1:1, 4:3 형태로 배치, 화면비.

예) 16:9 배치
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintDimensionRatio="16:9"

가로가 match_parent 크기 일 때 세로는 0dp 크기, Ratio 값의 영향으로 16:9로 배치.
가로가 160dp라면 세로는 90dp로 배치된다.




주의1 : 가로 세로 크기가 모두 고정된 값으로 지정된 경우 ratio는 적용되지 않는다.
주의2 : 1번의 layout_constraint을 적용하지 않으면 경고창이 표시된다.

This view is not constrained. It only has designtime positions, so it will jump to (0,0) at runtime unless you add the constraints less... (Ctrl+F1)
Inspection info:The layout editor allows you to place widgets anywhere on the canvas, and it records the current position with designtime attributes (such as layout_editor_absoluteX). These attributes are not applied at runtime, so if you push your layout on a device, the widgets may appear in a different location than shown in the editor. To fix this, make sure a widget has both horizontal and vertical constraints by dragging from the edge connections.  Issue id: MissingConstraints


- 레이아웃 샘플코드
<?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"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/textView"
        android:layout_width="160dp"
        android:layout_height="0dp"
        app:layout_constraintDimensionRatio="16:9"
        android:background="#d1c4e9"
        android:gravity="center"
        android:text="Hello World!"
        android:textSize="30dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />


</androidx.constraintlayout.widget.ConstraintLayout>





3. 레이아웃 예제




- 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"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.appcompat.widget.AppCompatTextView
        android:id="@+id/topImage"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:background="#d1c4e9"
        android:gravity="center"
        android:text="16:9"
        app:autoSizeTextType="uniform"
        app:layout_constraintDimensionRatio="16:9"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />


    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/userPanel"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@id/topImage">

        <androidx.appcompat.widget.AppCompatTextView
            android:id="@+id/userImage"
            android:layout_width="0dp"
            android:layout_height="100dp"
            android:background="@android:color/holo_blue_dark"
            android:gravity="center"
            android:text="1:1"
            app:autoSizeTextType="uniform"
            app:layout_constraintDimensionRatio="1:1"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <LinearLayout
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:orientation="vertical"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toRightOf="@id/userImage"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent">

            <androidx.appcompat.widget.AppCompatTextView
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:layout_weight="1"
                android:padding="12dp"
                android:text="UserId, autoSizeText"
                android:textColor="@android:color/black"
                app:autoSizeTextType="uniform" />

            <androidx.appcompat.widget.AppCompatTextView
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:layout_weight="1"
                android:padding="8dp"
                android:text="UserName, autoSizeText"
                app:autoSizeTextType="uniform" />
        </LinearLayout>
    </androidx.constraintlayout.widget.ConstraintLayout>

    <androidx.appcompat.widget.AppCompatTextView
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:background="@android:color/holo_green_dark"
        android:gravity="center"
        android:text="Text Field"
        android:textSize="30dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@id/userPanel" />

</androidx.constraintlayout.widget.ConstraintLayout>

2019년 3월 2일 토요일

안드로이드 앱 퍼미션 가이드

1. 퍼미션 확인
- AndroidManifest.xml 파일 내용 확인
- 붉은색 부분이 필요한 권한.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="simple.app.selfpermission">

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

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>


레퍼런스 : https://developer.android.com/guide/topics/permissions/overview?hl=ko




2. 권한 요청
- 권한 확인 : ContextCompat.checkSelfPermission
- 권한 요청 : ActivityCompat.requestPermissions

public void checkSelfPermission() {
String temp = "";
if (ContextCompat.checkSelfPermission(this,
Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
temp += Manifest.permission.WRITE_EXTERNAL_STORAGE + " ";
}

if (ContextCompat.checkSelfPermission(this,
Manifest.permission.READ_PHONE_STATE) != PackageManager.PERMISSION_GRANTED) {
temp += Manifest.permission.READ_PHONE_STATE + " ";
}

if (TextUtils.isEmpty(temp) == false) {
// 권한 요청
ActivityCompat.requestPermissions(this, temp.trim().split(" "), REQUEST_CODE_PERMISSIONS);
} else {
// 모두 허용 상태
}
}



3. 권한 요청 응답 확인
- onRequestPermissionsResult 오버라이드

@Override
public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) {
switch (requestCode) {
// 권한 처리
case REQUEST_CODE_PERMISSIONS: {
int length = permissions.length;
for (int i = 0; i < length; i++) {
if (grantResults[i] == PackageManager.PERMISSION_GRANTED) {
// 동의
Log.d(TAG, "PERMISSION_GRANTED : " + permissions[i]);
}
}
return;
}
default:
return;
}
}




4. 샘플코드
소스코드
APK



 

안드로이드9 HTTP Error - ERR_CLEARTEXT_NOT_PERMITTED

- 안드로이드 Pie 버전 부터 http 제한이 있습니다.
안드로이드 Pie : https://www.android.com/versions/pie-9-0/


1. ERR_CLEARTEXT_NOT_PERMITTED 오류
























WebView에서 http://www.google.com 으로 접속을 시도 하면 ERR_CLEARTEXT_NOT_PERMITTED 오류가 발생합니다.







2. 간단한 ERR_CLEARTEXT_NOT_PERMITTED 수정 방법
- AndroidManifest.xml 파일 수정.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="simple.app.httperror">

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

    <application
        android:usesCleartextTraffic="true"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

- 결과
























http://www.google.com 으로 접속을 시도하면 정상적으로 표시됩니다.
android:usesCleartextTraffic="true" 라인을 추가해서 처리하는 방법은 추천하지 않습니다.
3번 내용을 확인하고 올바른 방법으로 대응하는 것을 권장합니다.








3. 자세한 내용
https://android-developers.googleblog.com/2018/04/protecting-users-with-tls-by-default-in.html
https://developer.android.com/about/versions/pie/android-9.0-changes-28?hl=ko