앞선 글에서 Activity 생애주기(Life Cycle)를 고려하여 gallery와 gallery adapter를 onCreate()가 아니라 onStart()에 생성 및 연결을 했을 경우, camera intent 호출 후 새로 찍은 사진이 gallery에 정상 반영됨을 확인했다. 이를 보면서 사진을 삭제한 후에도 onStart()를 불러온다면 문제가 해결되겠다는 생각이 들었는데, 이를 구현하기 위해서 사진을 삭제하는 과정 자체를 새 intent로 불러왔다가 종료하여 MainActivity가 onRestart()가 되도록 해야했다. 그래서 이번에는 App. 내부에 새로운 activity를 추가해 intent로 활용하는 방법을 알아봤다. 

1. 목적 : Intent를 만들어 activity의 작동 구조를 이해하여 보자.

2. 개발 환경
 - PC : Windows 7, Android Studio 1.4.1(64-bit)
 - Phone : LG G pro Lollipop(API 21)

3. 참고자료
 1) Android Developers - Intent Reference (http://developer.android.com/reference/android/content/Intent.html)
 2) Android Developers - 인텐트 및 인텐트 필터 (http://developer.android.com/intl/ko/guide/components/intents-filters.html)

4. 과정
 일반적인 Intent 생성 및 작동 예제에 내가 필요한 기능을 추가했다.

 1) 새로 추가할 intent의 layout이 될 .xml 파일을 다음과 같이 '/layout' directory 밑에 생성한다. 필요에 따라 layout_main.xml을 활용하듯이 Button, ImageView, TextView 등을 추가하여 새로운 activity를 구성하면 된다.

 

2) Intent가 될 새 Activity를 생성한다. 'extends Activity'한 class java file을 다음과 같이 '/java' 밑에 만들고 MainActivity에서 onCreate() 내부에 내용을 쓰듯이 원하는 동작을 작성하면 된다. 나 같은 경우에는 단순히 AlertDialog를 사용하기 위해 intent를 불러오므로 따로 setContentView(R.layout.'intent layout file 명')하지 않아도 동작은 했었으나 일단 다음과 같이 코딩하였다.

public class FileRearrange extends AppCompatActivity{

    String delFilePath = null; // MainActivity에서 삭제할 file의 경로를 받을 String 변수 선언

    @Override
    protected void onCreate(Bundle savedIntanceState){
        super.onCreate(savedIntanceState);
        setContentView(R.layout.activity_file_rearrange);

        final Intent receiveIntent = getIntent(); // MainActivity에서 intent를 받음
        delFilePath = receiveIntent.getStringExtra("delFilePath"); // MainActivity의 intent에서 추가한 data 참조
        /* 이하 AlertDialog 활용 */
        AlertDialog.Builder builder = new AlertDialog.Builder(FileRearrange.this);
        builder.setTitle("파일을 삭제하시겠습니까?");
        builder.setNegativeButton("아니오", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                dialog.dismiss(); // 해당 AlertDialog의 dismiss() 및
                finish(); // receiveIntent의 finish();
            }
        });
        builder.setPositiveButton("예", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                File file = new File(delFilePath);
                file.delete(); // receive 받은 data를 활용해 file 삭제 후
                finish(); // receiveIntent의 finish();
            }
        });
        Dialog dialog = builder.create();
        dialog.show();
    }
}

 

 3) AndroidMainfest.xml 에 (2)에서 만든 Activity를 <application>...</application> 내부에 다음과 같이 <activity>...</activity>하여 선언한다. 이를 통해 App. 내부 Activity같의 communication이 가능해 진다.

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

    <uses-feature android:name="android.hardware.camera2" android:required="false" />

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


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

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <!-- 이하 activity 추가 부분 -->
        <activity android:name=".FileRearrange"> <!-- 위의 activity class명과 동일 -->
            <intent-filter>
                <action android:name="com.example.zip.customcameragalleryexample.filerearrange"/> <!-- intent 생성시 참조 -->
                <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>
        </activity>
        <!-- 이상 activity 추가 부분 -->
    </application>

</manifest>

 

 4) MainActivity.java 에서 파일을 삭제하고자 할 때 FileRearrange로 넘어가도록 다음과 같이 intent를 생성 및 추가한다. Intent 의 생성 → 필요할 경우 data 추가 → 해당 Intent의 start 순서로 넘어감을 이해하면 된다.

// CustomGallery 예시의 LongClickListener 부분을 다음과 같이 수정하였다.
      customGallery
.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() {
            @Override
            public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {

                final int tmpPosition = position;
                final String delFilePath = basePath + File.separator + imgs[tmpPosition];
                Intent intent = new Intent(MainActivity.this, FileRearrange.class);
                intent.putExtra("delFilePath", delFilePath);
                startActivity(intent);
                return false;
            }
        });

 

(위 Code의 실행 결과)
  원하는 동작이 오류 없이 실행되긴 했으나, 새로 intent를 불러오니 MainActivity 가 보이지 않았다. 그래서 OnItemLongClick 내부에서 file 삭제 후 다시 gallery를 불러오도록 해당 method를 재선언을 하자니 이 방법은 재귀적이라 app 자체가 비정상적으로 종료해서 쓸 수가 없었다. 아무래도 Adapter에서 배열의 변화를 감지하여 update가 되도록 notifyDataSetChanged() 와 같은 adapter의 method들을 다시 살펴볼 필요가 있어 보인다.

     

  

 

  사실 일전의 Custom Gallery Example에서 Gallery Item Long Click시 해당 사진 파일을 삭제하는 코드를 입력했었다. 실행 결과, 파일은 삭제되기는 했지만 파일 삭제 후의 Gallery Item들의 배열이 정리되지 않았고, 원래 해당 파일이 있던 frame 내부가 null 값으로 까맣게 보이며 click 시 App.이 비정상적으로 종료되었다. 그리고 Camera Intent로 촬영 후 새로 찍은 사진이 Gallery에 넘어오지 않았다. 아무래도 file 배열이 변경된 후 Gallery Adapter를 다시 불러와야 될 것 같은데, 원래 adapter가 생성되어 있던 onCreate()를 통째로 다시 불러들여도 괜찮을지 Activity 생애주기(Life Cycle)에 대해 알아봐야 겠다.

1. 목적 : Android Activity LifeCycle(Activity 생애주기)를 이해하고 기본 구조를 알아보자.

2. 개발 환경
 - PC : Windows 7, Android Studio 1.4.1(64-bit)
 - Phone : LG G pro Lollipop(API 21)

3. 참고자료
 1) Android Developers - Activity Reference (http://developer.android.com/reference/android/app/Activity.html)

4. Activity Life Cycle(Acitivity 생애 주기)의 이론

  Android Studio로 App. project를 생성해 보면 /java/'해당 packaging 명'/MainActivity.java 이 기본으로 만들어져 있고 그 안에 파일명과 같은 class가 'extends Activity' 혹은 'extends AppCompatAcitivity'을 끌고 기본 method인 onCreate(Bundle savedInstanceState)를 @Override한 채로 만들어져 있는 광경을 볼 수 있다. 이 모습을 보다보면 아무래도 C 언어에서 void main(void) 가 생성되며 시작 지점이 자동으로 지정되듯이 onCreate()가 필수적 임을 눈치로 대강 알 수 있을텐데, 실제로 'Android Developers reference'를 찾아보면 다음과 같은 순서도를 통해서 App.의 Activity에 대한 동작 구조를 파악할 수 있다.

  보시다시피 App.이 실행되면서 Activity가 시작되면 처음에 onCreate()를 통해서 Activity가 생성되고 onStart()와 onResume()을 거처 해당 Activity가 실행된다. 그러다가 해당 Activity를 sleep 시킬 때 onPause()와 onStop()을 거치게 되고, onStop() method 시점에서 사용자가 해당 Activity를 다시 실행시키고자 하면 onRestart()를, App.이 memory를 많이 잡아먹어 시스템에 의해 process kill이 됐을 때 사용자가 원할 경우 onCreate()로 넘어간다. 이런 method들을 돌고 돌던 Activity는 끝날 때가 되면 onDestory()를 통해 system에서 사라진다.

  그래서 App.의 activity의 생애주기에 따라 작동해야 할 특정한 기능이 있다면 아래와 같이 Activity class 내에 위 method()를 따로 정의해서 사용한다. 하나의 예로서, 이전에 항상 꺼지지 않는 App.을 만들기 위해서 onStop() 을 @Override하여 onStop() 내에서 onRestart()를 무조건 실행시키게 만들었었다.

public class Activity extends ApplicationContext {
    protected void onCreate(Bundle savedInstanceState);

    protected void onStart();

    protected void onRestart();

    protected void onResume();

    protected void onPause();

    protected void onStop();

    protected void onDestroy();
}

  그럼 이번 Gallery가 새로 찍은 사진이 update되지 않는 문제는 Gallery Adapter의 생성 및 연결을 onStart() method에 선언하여 @Override 해보면 되지 않을까? 아무래도 Camera Intent를 불러오면서 기존 MainActivity는 onStop()에 있어 intent가 종료되고 나면 onStart() 부터 다시 시작할 것 같았기 때문이다.

5. 실험

  이전의 Custom Gallery Example의 Gallery Adapter 선언 부분을 단순히 onStart()로 아래와 같이 옮겨봤다.

public class MainActivity extends AppCompatActivity {

    public static final int MEDIA_TYPE_IMAGE = 1;
    public static final int MEDIA_TYPE_VIDEO = 2;
    public int inSampleSize = 1;

    private static final int CAPTURE_IMAGE_ACTIVITY_REQUEST_CODE = 100;
    private Uri fileUri;
    private static String basePath;

    public float imageViewRotation = 90;
    public String TAG = "Camera Example :: ";

    private Button takePicBtn;
    private ImageView resultView;
    private TextView imgPath;
    private Gallery customGallery;
    private CustomGalleryAdapter customGalAdapter;

    private String[] imgs;

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

        File mediaStorageDir = new File(Environment.getExternalStoragePublicDirectory(
                Environment.DIRECTORY_PICTURES), "MyCameraApp");
        // This location works best if you want the created images to be shared
        // between applications and persist after your app has been uninstalled.

        // Create the storage directory if it does not exist
        if (! mediaStorageDir.exists()){
            if (! mediaStorageDir.mkdirs()){
                Log.d("MyCameraApp", "failed to create directory");

            }
        }

        basePath = mediaStorageDir.getPath();

        imgPath = (TextView)findViewById(R.id.imgpath);
        resultView = (ImageView)findViewById(R.id.resultview);
        takePicBtn = (Button)findViewById(R.id.takepicbtn);
        takePicBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // create Intent to take a picture and return control to the calling application
                Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);

                fileUri = getOutputMediaFileUri(MEDIA_TYPE_IMAGE); // create a file to save the image
                intent.putExtra(MediaStore.EXTRA_OUTPUT, fileUri); // set the image file name

                // start the image capture Intent
                startActivityForResult(intent, CAPTURE_IMAGE_ACTIVITY_REQUEST_CODE);
            }
        });

       
        customGallery = (Gallery)findViewById(R.id.customgallery);


    }

    /* 아래와 같이 onStart()를 Override하여 Gallery Adapter 와 연관된 부분을 그대로 복사해 옮겨놓았다. */
    @Override
    protected void onStart(){
        super.onStart();

        File file = new File(basePath);
        imgs = file.list();
        for(int i=0; i<imgs.length; i++){
            imgPath.setText(imgs[i]);
        }

        customGalAdapter = new CustomGalleryAdapter(getApplicationContext(), basePath);
        customGallery.setAdapter(customGalAdapter);
        customGallery.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                Bitmap bm = BitmapFactory.decodeFile(basePath+ File.separator +imgs[position]);
                Bitmap bm2 = ThumbnailUtils.extractThumbnail(bm, bm.getWidth() / inSampleSize, bm.getHeight() / inSampleSize);
                resultView.setImageBitmap(bm2);
                imgPath.setText(basePath+File.separator+imgs[position]);
            }
        });
        customGallery.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() {
            @Override
            public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
                File temp = new File(basePath+File.separator+imgs[position]);
                temp.delete();
                return false;
            }
        });
    }

  위 변경사항을 반영하여 compile/Run을 시켜봤더니 다행히 camera intent 실행 후 새로 찍은 사진이 gallery 에 반영되었다. 이제는 파일 삭제 후 Gallery update가 되도록 다른 method를 살펴봐야겠다.

(위 code의 실행결과)

     

 

1. 목표 필요한 File을 생성하고 해당 File의 경로(Path)를 확인한다.

2. 개발 환경
  - Android Studio 1.4.1(64bit)
  - LG G pro (LolliPop)

3. App Coding 과정
 1) AndroidManifest.xml 의 수정
   : 해당 App.이 File을 read/write할 권한을 주기 위해 <uses-permission ...>을 다음과 같이 준다. 이 과정을 수행하지 않을 경우, compile 및 실행은 문제 없이 되나 실제 File을 생성되지 않는다.

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.zip.filepathexample" >

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

    <application ...

 

 2) .xml file에 파일 생성 여부를 보여 주기 위한 Layout 설정
   : 생성된 File의 경로를 보여주기 위해 TextView를 사용하기로 했다. 이 부분은 추후에 List 등을 이용하여 icon화 시키는 등 응용하는 방법을 배워야 겠다.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:paddingBottom="@dimen/activity_vertical_margin" tools:context=".MainActivity">

    <TextView
        android:id="@+id/pathview01"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="5dp"
        android:text="Path01"/>
    <TextView
        android:id="@+id/pathview02"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="5dp"
        android:text="Path02"/>
    <TextView
        android:id="@+id/pathview03"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="5dp"
        android:text="Path03"/>

</LinearLayout>

 

 3) .java file을 통한 Activity 설정
   a. external storage에 file directory 생성
    : File 생성자를 통해 다음과 같이 file을 저장할 directory를 생성한다.

File storeDir = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS), "MyDocApp");
if (!storeDir.exists()) {
    if (!storeDir.mkdirs()) {
        Log.d("MyDocApp", "failed to create directory");
        return;
    }
}

 

  b. file 생성
   : 실제 생성할 File을 다음과 같이 생성한다. file name은 해당 명령이 수행되는 시간을 따와서 쓸 수 있도록 SimpleDateFormat().format(new Date());를 사용한다.

timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
temp = new File(storeDir.getPath() + File.separator + "test_"+ timeStamp + ".docx");
if(temp == null){
    Log.d(TAG, "Error at creating .docx file, check storage permissions :");
    return;
}

 

  c. file 생성 경로의 확인
    : 생성된 파일의 경로를 String으로 찍어 봄으로서 파일의 생성 여부를 확인한다. 이 때, 'file 생성자 변수명'.getPath(); 혹은 .getAbsolutePath();, .getCanonicalPath(); 등 file 내 경로 확인 함수가 여러 개가 있었는데 string 값에는 차이가 없었으나, 실행 장치나 compile 방식에 따라 차이가 있을 수도 있다(참고 : http://egloos.zum.com/entireboy/v/4196432).

pathView01.setText(temp.getAbsolutePath());
pathView02.setText(temp.getPath());
try {
    pathView03.setText(temp.getCanonicalPath());
} catch (IOException e) {
    Log.d(TAG, "Error at creating .docx file, check storage permissions :");
    return;
}

 

4. App 생성 결과
  아래 사진과 같이 file path를 확인했는데 android 내에서는 getPath()나 getAbsolutePath(), getCanonicalPath();들의 차이가 확인되지 않았다. 다른 사람들을 블로그를 보면 다소 차이가 있었는데, 아무래도 나는 file을 생성할 때 경로를 모두 찍어 입력해서 그렇지 않은가 싶다.

 

MainActivity만 쓰다가 다른 Activity를 추가해 새 화면을 사용하고자 했다.

추가할 Activity의 구조는 처음에 생성되는 MainActivity를 보고 class명을 변경해서 눈치껏 변경했는데 다른 파일들도 수정하거나 추가해야 했다.

기본서와 구글링을 통해 얻은 정보를 추려서 정리하고자 한다.

(p.s. 이해를 돕는 image는 추후에 등록하겠습니다.)


1. 'AndroidManifest.xml'에 추가하고자 하는 sub-Activity를 등록한다.

<manifest ....>

<application android:name=".MyApplication" ...>

<activity android:name=".MainActivity" ...></activity>

<activity android:name=".AddActivity" ...></activity> // 미리 Activity에 해당하는 class을 생성해 놓으면 자동완성이 되어서 편했다.

</application>

</manifest>


2. 생성하고자 하는 추가 Activity class를 생성한다.

2-1) 위 1.과 같은 경우 Java->'생성한 App' folder를 선택한 후

     마우스 오른쪽 버튼을 통해 New -> Java Class에서 AddActivity 라는 이름으로 등록하면 자동으로 activity class가 생성된다.

2-2) 생성된 class 명 옆에 'extends Activity'와 내부에 'onCreate' method를 추가한다.

public class AddActivity extends Activity {


// 필요한 부분 추가


@Override

public void onCreate(Bundle savedInstanceState){

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_addActivity); // 사실 지금 당장은 이 부분에서 error가 날 것이다. activity_addActivity.xml 파일을 추가해 줘야 한다.


// 필요한 부분 추가

}

}

2-3) 위 class에서 사용할 activity_addActivity.xml 파일을 layout folder(activity_main.xml이 있는 위치와 동일)에 생성해 준다.


3. 본래 사용하던 Activity로 돌아와 Intent를 선언해 준다.

public void MainActivity extends Activity {


// 생략


@Override

public void onCreate(Bundle savedInstanceState){

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_main);


// 생략


Intent intent = new Intent(); // intent의 선언
ComponentName name = new ComponentName("package 명 입력", "package명.AddActivity");
intent.setComponent(name); // intent에 component를 등록
startActivity(intent); // 해당 intent 시작. 위에 등록한 AddActivity가 실행됨

}

}


4. 추가한 Activity에서 다시 MainActivity와 같은 본래 Activity로 돌아올 수 있도록 'finish();'를 추가한다.

public class AddActivity extends Activity {


// 필요한 부분 추가


@Override

public void onCreate(Bundle savedInstanceState){

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_addActivity); // 사실 지금 당장은 이 부분에서 error가 날 것이다. activity_addActivity.xml 파일을 추가해 줘야 한다.


// 필요한 부분 추가


finish(); // 편의에 따라서 보통 Button등을 통해 해당 button을 누를 경우 돌아가도록 등록하기도 한다.

}

}

+ Recent posts