2017년 3월 11일 토요일

Making file explorer in Android ( 파일 탐색기 만들기 )

서론

파일 탐색기를 만드는건 앞에서 설명한 custom list 위에 간단하게 파일 처리만 넣으면 됩니다.

일단 최종 실행 화면을 보면 이해가 쉽습니다.


전체적인 동작은 기본적으로 커스텀 리스트를 사용한 소스 위에 작업할 예정입니다. 따라서 이전 작업한 아래 링크를 바탕으로 하기 때문에 아직 아래 링크를 확인 안하셨으면 먼저 보시기 바랍니다.
http://swlock.blogspot.com/2017/03/custom-list-which-has-check-box-in.html


권한 처리

안드로이드 M OS 이상부터 내부 저장소(sdcard) 경로에 대한 Runtime permission이 추가 되었습니다.

AndroidManifest.xml 에 예전에는 아래와 같이 추가하면되었었는데요.
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

이것외에 Runtime 추가 작업이 필요합니다. setupPermission 이라는 메소드를 만들었습니다. SDK 버전을 검사해서 M OS 이상일때 READ_EXTERNAL_STORAGE 권한이 DENIED가 나온다면 권한을 요청하는 requestPermissions 메소드를 호출합니다.
    private void setupPermission() {
        //check for permission
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            if (ContextCompat.checkSelfPermission(this,
                    Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) {
                //ask for permission
                requestPermissions(new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, 0);
            }
        }
    }

사용자가 허용, 거부를 하면 결과가 아래 메소드가 호출 됩니다.
    public void onRequestPermissionsResult(int requestCode,
                                           String permissions[], int[] grantResults) {
        setupAdapter();
    }
그래서 여기에서는 허용할때를 대비하여 setupAdater를 호출해서 리스트 갱신을 다시한번 시도 합니다. (권한이 없어서 못했을 수도 있으니...)

List 에 목록 만들기

4개의 변수를 준비합니다. root는 최상위 폴더가 되고, CurPath는 현재 탐색하는 폴더가 됩니다.
itemFiles는 화면에 display되는 파일이나 폴더 이름이 되고, pathFiles list는 화면에 display되는 list의 경로와 이름이 붙어있는 목록이 됩니다.
    private String root= Environment.getExternalStorageDirectory().getAbsolutePath();
    private String CurPath=Environment.getExternalStorageDirectory().getAbsolutePath();
    private ArrayList<String> itemFiles = new ArrayList<String>();
    private ArrayList<String> pathFiles = new ArrayList<String>();

List를 만들어내는것은 doInBackground에서 처리 한다고 보면 됩니다. listDispItems list에 값을 넣어야 하는데 listAllItems 의 item들이 sort되어 들어가게 됩니다.
itemFiles, pathFiles들이 listAllItems에 추가가 되는데 이값들은 getDirInfo메소드 결과에 의해서 생성됩니다.
        protected String doInBackground(String... strings) {
            // listAllItems MyListItem
            listAllItems.clear();
            listDispItems.clear();

            getDirInfo(CurPath);

            for(int i=0;i<itemFiles.size();i++){
                MyListItem item = new MyListItem();
                item.checked = false;
                item.name = itemFiles.get(i);
                item.path = pathFiles.get(i);
                listAllItems.add(item);
            }

            if (listAllItems != null) {
                Collections.sort(listAllItems, nameComparator);
            }
            listDispItems.addAll(listAllItems);
            return null;
        }

폴더 탐색

getDirInfo는 주어진 경로의 파일이나 폴더를 탐색하게 됩니다. 그리고 그결과를 itemFiles, pathFiles에 추가를 하게됩니다. itemFiles는 눈에 보이는 결과만 저장하고 pathFiles는 path와 이름을 같이 저장해서 폴더를 이동하는 용도로 사용합니다. 그리고 읽은 파일이 폴더이면 뒤쪽에 / 을 붙였습니다.
    private void getDirInfo(String dirPath)
    {
        if(!dirPath.endsWith("/")) dirPath = dirPath+"/";
        File f = new File(dirPath);
        File[] files = f.listFiles();
        if( files == null ) return;

        itemFiles.clear();
        pathFiles.clear();

        if( !root.endsWith("/") ) root = root+"/";
        if( !root.equals(dirPath) ) {
            itemFiles.add("../");
            pathFiles.add(f.getParent());
        }

        for(int i=0; i < files.length; i++){
            File file = files[i];
            pathFiles.add(file.getPath());

            if(file.isDirectory())
                itemFiles.add(file.getName() + "/");
            else
                itemFiles.add(file.getName());
        }
    }

list에서 값을 눌렀을때 처리는 itemClick이 호출되는데, 호출된 쪽에서는 name, path 를 같이 인자로 넘겨줍니다.
            TextView name = (TextView)retView.findViewById(R.id.name_saved);
            name.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    MyListItem item = listDispItems.get(pos);
                    itemClick(item.name,item.path);
                }
            });

폴더 이동

name과 path를 받아서 만약 path가 폴더라면 CurPath를 변경해서 setupAdapter()메소드를 호출하여 list목록을 새로 구성합니다.
    private void itemClick(String name, String path) {
        File file = new File(path);
        if (file.isDirectory())
        {
            if(file.canRead()) {
                CurPath = path;
                setupAdapter();
            }else{
                new AlertDialog.Builder(this)
                        .setTitle("[" + file.getName() + "] folder can't be read!")
                        .setPositiveButton("OK",
                                new DialogInterface.OnClickListener() {
                                    public void onClick(DialogInterface dialog, int which) {
                                    }
                                }).show();
            }
        }
        else{
            new AlertDialog.Builder(this)
                    .setTitle("[" + file.getName() + "]")
                    .setPositiveButton("OK",
                            new DialogInterface.OnClickListener() {
                                public void onClick(DialogInterface dialog, int which) {
                                }
                            }).show();
        }
    }





지금까지 작업된 전체 소스 입니다.
일부 소스는 이전과 같습니다.
MainActivity.java
package com.app.xxx.customlistexample;
import android.Manifest; import android.app.ProgressDialog; import android.content.Context; import android.content.DialogInterface; import android.content.pm.PackageManager; import android.os.AsyncTask; import android.os.Build; import android.os.Environment; import android.support.v4.content.ContextCompat; import android.support.v7.app.AlertDialog; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.text.Editable; import android.text.TextWatcher; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.CheckBox; import android.widget.EditText; import android.widget.ListView; import android.widget.TextView; import android.widget.Toast; import java.io.File; import java.text.Collator; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; public class MainActivity extends AppCompatActivity { int totalSelected = 0; EditText editView; ListView listView; TextView textView; MyListAdapterMy listadapter; ArrayList<MyListItem> listAllItems=new ArrayList<MyListItem>(); ArrayList<MyListItem> listDispItems=new ArrayList<MyListItem>(); private String root= Environment.getExternalStorageDirectory().getAbsolutePath(); private String CurPath=Environment.getExternalStorageDirectory().getAbsolutePath(); private ArrayList<String> itemFiles = new ArrayList<String>(); private ArrayList<String> pathFiles = new ArrayList<String>(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); textView=(TextView)findViewById(R.id.textView_debug); setupList(); setupAdapter(); setupFilter(); setupPermission(); } private void setupPermission() { //check for permission if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) { //ask for permission requestPermissions(new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, 0); } } } @Override public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) { setupAdapter(); } private void setupList() { listView=(ListView)findViewById(R.id.list1); } public class MyListItem { int selectedNumber; boolean checked; // for display String name; // for using ; path+name String path; } public class MyListAdapterMy extends MyArrayAdapter<MyListItem> { public MyListAdapterMy(Context context) { super(context,R.layout.list_item); totalSelected = 0; setSource(listDispItems); } @Override public void bindView(View view, MyListItem item) { TextView name = (TextView)view.findViewById(R.id.name_saved); name.setText(item.name); CheckBox cb = (CheckBox)view.findViewById(R.id.checkBox_saved); cb.setChecked(item.checked); } @Override public View getView(int position, View convertView, ViewGroup parent) { View retView = super.getView(position,convertView,parent); final int pos = position; final View parView = retView; CheckBox cb = (CheckBox)retView.findViewById(R.id.checkBox_saved); cb.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { MyListItem item = listDispItems.get(pos); item.checked = !item.checked; if( item.checked ) totalSelected++; item.selectedNumber=totalSelected; Toast.makeText(MainActivity.this,"1: Click "+pos+ "th " + item.checked + " "+totalSelected,Toast.LENGTH_SHORT).show(); printDebug(); } }); TextView name = (TextView)retView.findViewById(R.id.name_saved); name.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { MyListItem item = listDispItems.get(pos); //item.checked = !item.checked; //if( item.checked ) totalSelected++; //item.selectedNumber=totalSelected; //Toast.makeText(MainActivity.this,"2: Click "+pos+ "th " + item.checked + " "+totalSelected,Toast.LENGTH_SHORT).show(); //printDebug(); //bindView(parView, item); itemClick(item.name,item.path); } }); return retView; } public void fillter(String searchText) { listDispItems.clear(); totalSelected = 0; for(int i = 0;i<listAllItems.size();i++){ MyListItem item = listAllItems.get(i); item.checked = false; item.selectedNumber = 0; } if(searchText.length() == 0) { listDispItems.addAll(listAllItems); } else { for( MyListItem item : listAllItems) { if(item.name.contains(searchText)) { listDispItems.add(item); } } } notifyDataSetChanged(); } } public class AdapterAsyncTask extends AsyncTask<String,Void,String> { private ProgressDialog mDlg; Context mContext; public AdapterAsyncTask(Context context) { mContext = context; } @Override protected void onPreExecute() { super.onPreExecute(); mDlg = new ProgressDialog(mContext); mDlg.setProgressStyle(ProgressDialog.STYLE_SPINNER); mDlg.setMessage( "loading" ); mDlg.show(); } @Override protected String doInBackground(String... strings) { // listAllItems MyListItem listAllItems.clear(); listDispItems.clear(); getDirInfo(CurPath); for(int i=0;i<itemFiles.size();i++){ MyListItem item = new MyListItem(); item.checked = false; item.name = itemFiles.get(i); item.path = pathFiles.get(i); listAllItems.add(item); } if (listAllItems != null) { Collections.sort(listAllItems, nameComparator); } listDispItems.addAll(listAllItems); return null; } @Override protected void onPostExecute(String s) { super.onPostExecute(s); mDlg.dismiss(); listadapter=new MyListAdapterMy(mContext); listView.setAdapter(listadapter); String searchText = editView.getText().toString(); if( listadapter!=null ) listadapter.fillter(searchText); textView.setText("Location: " + CurPath); } private final Comparator<MyListItem> nameComparator = new Comparator<MyListItem>() { public final int compare(MyListItem a, MyListItem b) { return collator.compare(a.name, b.name); } private final Collator collator = Collator.getInstance(); }; } private void setupAdapter() { AdapterAsyncTask adaterAsyncTask = new AdapterAsyncTask(MainActivity.this); adaterAsyncTask.execute(); } private void setupFilter() { editView=(EditText)findViewById(R.id.savedfilter); editView.addTextChangedListener(new TextWatcher() { @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { } @Override public void onTextChanged(CharSequence s, int start, int before, int count) { } @Override public void afterTextChanged(Editable s) { String searchText = editView.getText().toString(); if( listadapter!=null ) listadapter.fillter(searchText); } }); } private int getSelectedItemCount() { int checkcnt = 0; for(int i=0;i<listDispItems.size();i++){ MyListItem item = listDispItems.get(i); if( item.checked ) checkcnt++; } return checkcnt; } private List<String> getSelectedItems() { List<String> ret = new ArrayList<String>(); int count = 0; for(int i=0;i<listDispItems.size();i++){ MyListItem item = listDispItems.get(i); if( item.checked ) { if( count < item.selectedNumber ){ count = item.selectedNumber; } } } for(int j=1;j<=count;j++) { for (int i = 0; i<listDispItems.size() ;i++ ){ MyListItem item = listDispItems.get(i); if( item.checked && item.selectedNumber == j){ ret.add(item.name); } } } return ret; } private String getSelectedItem() { List<String> ret = new ArrayList<String>(); for(int i=0;i<listDispItems.size();i++){ MyListItem item = listDispItems.get(i); if( item.checked ) { return item.name; } } return ""; } private void printDebug() { StringBuilder sb = new StringBuilder(); sb.append("Count:"+getSelectedItemCount()+"\n"); sb.append("getSelectedItem:"+getSelectedItem()+"\n"); sb.append("getSelectedItems:"); List<String> data = getSelectedItems(); for(int i=0;i<data.size();i++){ String item = data.get(i); sb.append(item+","); } //textView.setText(sb.toString()); } private void getDirInfo(String dirPath) { if(!dirPath.endsWith("/")) dirPath = dirPath+"/"; File f = new File(dirPath); File[] files = f.listFiles(); if( files == null ) return; itemFiles.clear(); pathFiles.clear(); if( !root.endsWith("/") ) root = root+"/"; if( !root.equals(dirPath) ) { itemFiles.add("../"); pathFiles.add(f.getParent()); } for(int i=0; i < files.length; i++){ File file = files[i]; pathFiles.add(file.getPath()); if(file.isDirectory()) itemFiles.add(file.getName() + "/"); else itemFiles.add(file.getName()); } } private void itemClick(String name, String path) { File file = new File(path); if (file.isDirectory()) { if(file.canRead()) { CurPath = path; setupAdapter(); }else{ new AlertDialog.Builder(this) .setTitle("[" + file.getName() + "] folder can't be read!") .setPositiveButton("OK", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { } }).show(); } } else{ new AlertDialog.Builder(this) .setTitle("[" + file.getName() + "]") .setPositiveButton("OK", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { } }).show(); } } }

AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.app.xxx.customlistexample">
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <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">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

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

</manifest>

MyArrayAdapter.java
package com.app.xxx.customlistexample;

import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;

import java.util.List;

public abstract class MyArrayAdapter<E> extends BaseAdapter
{
    public MyArrayAdapter(Context context, int layoutRes) {
        mContext = context;
        mInflater = (LayoutInflater)context.getSystemService(
                Context.LAYOUT_INFLATER_SERVICE);
        mLayoutRes = layoutRes;
    }

    public void setSource(List<E> list) {
        mList = list;
    }

    public abstract void bindView(View view, E item);

    public E itemForPosition(int position) {
        if (mList == null) {
            return null;
        }

        return mList.get(position);
    }

    public int getCount() {
        return mList != null ? mList.size() : 0;
    }

    public Object getItem(int position) {
        return position;
    }

    public long getItemId(int position) {
        return position;
    }

    public View getView(int position, View convertView, ViewGroup parent) {
        View view;
        if (convertView == null) {
            view = mInflater.inflate(mLayoutRes, parent, false);
        } else {
            view = convertView;
        }
        bindView(view, mList.get(position));
        return view;
    }

    private final Context mContext;
    private final LayoutInflater mInflater;
    private final int mLayoutRes;
    private List<E> mList;
}

activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.app.darts.customlistexample.MainActivity">

    <LinearLayout
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <LinearLayout
            android:orientation="horizontal"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_alignParentStart="true">

            <ImageView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                app:srcCompat="@android:drawable/ic_menu_search"
                android:id="@+id/imageView" />

            <EditText
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:inputType="textPersonName"
                android:text=""
                android:ems="10"
                android:id="@+id/savedfilter" />

        </LinearLayout>

        <LinearLayout
            android:orientation="vertical"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content">

            <TextView
                android:text="TextView"
                android:layout_width="match_parent"
                android:layout_height="102dp"
                android:id="@+id/textView_debug"
                android:maxLines="5" />
            <ListView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:id="@+id/list1"
                />


        </LinearLayout>

    </LinearLayout>


</RelativeLayout>

list_item.xml
<?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="wrap_content"
    android:minHeight="?android:attr/listPreferredItemHeight"
    android:orientation="vertical"
    android:gravity="fill" >

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_weight="1"
        android:orientation="horizontal"
        android:paddingRight="6dip"
        android:paddingLeft="6dip"
        android:gravity="center_vertical" >

        <CheckBox
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:id="@+id/checkBox_saved"
            android:layout_marginLeft="5dip"
            android:layout_marginRight="11dip"
            android:layout_gravity="center_vertical"
            android:scaleType="fitCenter"/>

        <LinearLayout
            android:orientation="vertical"
            android:layout_width="match_parent"
            android:layout_height="match_parent" >
            <TextView android:id="@+id/name_saved"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:layout_weight="1"
                android:textAppearance="?android:attr/textAppearanceMedium"
                android:singleLine="false"
                android:ellipsize="marquee"
                android:layout_marginBottom="2dip"
                android:maxLines="3"
                android:textSize="12sp"
                android:gravity="center_vertical" />
        </LinearLayout>
    </LinearLayout>
</LinearLayout>


https://drive.google.com/open?id=0B9vAKDzHthQIeXNqcWNlNzc3NjA

google drive가 권한이 필요하여 새로 작업하였습니다.

전체 소스 다운로드는 아래 게시물 참고 하시기 바랍니다. 

댓글 없음:

댓글 쓰기