728x90

Android 에서 엑셀 파일 내보내기를 테스트하고 적어둔다.


테스트 환경 : Android 8.0, Android Studio 3.2.1


1. 엑셀 라이브러리 추가하기

    - http://poi.apache.org/download.html#POI 에서 POI(Binary Distribution) 최신 버전을 다운로드 한다.

    - zip 파일을 받은 걸 압축을 풀면 poi-4.0.1.jar 파일이 보인다.

    - 이 파일을 해당 프로젝트의 libs 폴더에 복사한다.

apply plugin: 'com.android.application'

android {
    compileSdkVersion 27

    defaultConfig {
        applicationId "com.tistory.android.excel"
        minSdkVersion 19
        targetSdkVersion 27
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"

    }

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }

}

dependencies {
    implementation fileTree(include: ['*.jar'], dir: 'libs')
    implementation 'com.android.support:appcompat-v7:27.1.1'
    implementation 'com.android.support.constraint:constraint-layout:1.1.3'
    implementation 'com.android.support:support-v4:27.1.1'
    implementation 'com.android.support:recyclerview-v7:27.1.1' // ListView 개선 버전
    implementation 'com.android.support:cardview-v7:27.1.1'
    implementation 'com.android.support:design:27.1.1'
    implementation 'gun0912.ted:tedpermission:2.0.0'
    implementation 'com.android.volley:volley:1.1.0'
    implementation 'com.github.bumptech.glide:glide:3.8.0' // 이미지 라이브러리
    implementation files('libs/poi-4.0.1.jar')
}


2. 컴파일 에러 발생

    Invoke-customs are only supported starting with android 0 --min-api 26

  

    구글링 검색 결과 build.gradle 에 아래 코드 추가 (Java 버전에 맞게 추가 필요)

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }


3. 파일 내보내기 에러 발생

android.os.FileUriExposedException: file:///storage/emulated/0/Android/data/com.tistory.android.excel/files/test.xls exposed beyond app through ClipData.Item.getUri()


해결방안

- Android Manifest.xml 파일에 아래 provider 코드를 추가한다.

- res/xml/provider_paths.xml 파일을 생성한다.

- Java 코드를 아래와 같이 추가한다.

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

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
    <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

    <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>

        <provider
            android:name="android.support.v4.content.FileProvider"
            android:authorities="${applicationId}.fileprovider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/provider_paths"
                tools:replace="android:resource" />
        </provider>

    </application>

</manifest>

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <external-path name="excel" path="." />
</paths>

if (Build.VERSION.SDK_INT >= 24) { // Android Nougat ( 7.0 ) and later
    Uri uri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".fileprovider",xlsFile);
    Intent intent = new Intent(Intent.ACTION_SEND);
    intent.setDataAndType(uri, "application/excel");
    intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
    intent.putExtra(Intent.EXTRA_STREAM,uri);
    startActivity(Intent.createChooser(intent,"엑셀 내보내기"));
} else {
    Uri uri = Uri.fromFile(xlsFile);
    Intent intent = new Intent(Intent.ACTION_SEND);
    intent.setType("application/excel");
    intent.putExtra(Intent.EXTRA_STREAM,uri);
    startActivity(Intent.createChooser(intent,"엑셀 내보내기"));
}

// 미디어 스캐닝 (폴더내에 생성된 파일 보이도록 하는 코드)
MediaScannerConnection.scanFile(getApplicationContext(), new String[]{xlsFile.getAbsolutePath()}, null, new MediaScannerConnection.OnScanCompletedListener() {
    @Override
    public void onScanCompleted(String s, Uri uri) {
    }
});
 


구글링으로 찾은 코드가 Android Nougat(7.0 이상)에서 에러가 발생하여 안드로이드 7.0 이상에서 에러 해결 코드로 보완을 했다.

셀 파일 저장 로직은 동일하지만 다른 코드들은 다르다는 걸 알 수 있을 것이다.


테스트에 사용된 전체 코드

package com.tistory.android.excel;

import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.media.MediaScannerConnection;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.support.design.widget.FloatingActionButton;
import android.support.v4.content.FileProvider;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Toast;

import com.gun0912.tedpermission.PermissionListener;
import com.gun0912.tedpermission.TedPermission;
import com.tistory.android.common.BackPressHandler;

import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;

public class MainActivity extends AppCompatActivity {
    Context context;
    ArrayList<String> aryName = new ArrayList<>();
    ArrayList<String> aryAge = new ArrayList<>();

    String exportFolderName = "/DCIM/Excel";
    String outputFileName = "test.xls";
    File xlsFile;

    private BackPressHandler backPressHandler;

    PermissionListener permissionlistener = new PermissionListener() {
        @Override
        public void onPermissionGranted() {
            initView();
        }

        @Override
        public void onPermissionDenied(ArrayList<String> deniedPermissions) {
            Toast.makeText(MainActivity.this, "권한 허용을 하지 않으면 서비스를 이용할 수 없습니다.", Toast.LENGTH_SHORT).show();
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        context = this.getBaseContext();
        backPressHandler = new BackPressHandler(this); // 뒤로 가기 버튼 이벤트

        // 네트워크 연결상태 체크
        if (NetworkConnection() == false) NotConnected_showAlert();
        checkPermissions();
    }

    private void checkPermissions() {
        if (Build.VERSION.SDK_INT >= 23) { // 마시멜로(안드로이드 6.0) 이상 권한 체크
            TedPermission.with(context)
                    .setPermissionListener(permissionlistener)
                    .setRationaleMessage("앱을 이용하기 위해서는 접근 권한이 필요합니다")
                    .setDeniedMessage("앱에서 요구하는 권한설정이 필요합니다...\n [설정] > [권한] 에서 사용으로 활성화해주세요.")
                    .setPermissions(new String[]{
                            //android.Manifest.permission.READ_PHONE_STATE,
                            //android.Manifest.permission.CALL_PHONE,  // 전화걸기 및 관리
                            //android.Manifest.permission.WRITE_CONTACTS, // 주소록 액세스 권한
                            android.Manifest.permission.READ_EXTERNAL_STORAGE,
                            android.Manifest.permission.WRITE_EXTERNAL_STORAGE // 기기, 사진, 미디어, 파일 엑세스 권한
                            //android.Manifest.permission.RECEIVE_SMS //, // 문자 수신
                            //android.Manifest.permission.CAMERA
                    })
                    .check();

        } else {
            initView();
        }
    }

    private void initView() {
        initData(); // 읽어온 데이터라고 가정하고 데이터 생성

        FloatingActionButton fab =  findViewById(R.id.fab);
        fab.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                saveExcel();
            }
        });
    }

    private void initData(){ // 엑셀에 저장여부만 확인 목적으로 간단하게 추가하는 코드로 작성
        aryName.add("강감찬"); aryAge.add("23");
        aryName.add("이순신"); aryAge.add("57");
        aryName.add("홍길동"); aryAge.add("34");
        aryName.add("이방원"); aryAge.add("53");
        aryName.add("고주몽"); aryAge.add("73");
        aryName.add("유관순"); aryAge.add("68");
    }

    private void saveExcel(){
        Workbook workbook = new HSSFWorkbook();
        Sheet sheet = workbook.createSheet(); // 새로운 시트 생성
        Row row = sheet.createRow(0); // 새로운 행 생성
        Cell cell;

        cell = row.createCell(0); // 1번 셀 생성
        cell.setCellValue("이름"); // 1번 셀 값 입력

        cell = row.createCell(1); // 2번 셀 생성
        cell.setCellValue("나이"); // 2번 셀 값 입력

        cell = row.createCell(2); // 3번 셀 생성
        cell.setCellValue("전화번호"); // 3번 셀 값 입력

        for(int i = 0; i < aryName.size() ; i++){ // 데이터 엑셀에 입력
            row = sheet.createRow(i+1);

            cell = row.createCell(0);
            cell.setCellValue(aryName.get(i));

            cell = row.createCell(1);
            cell.setCellValue(aryAge.get(i));
        }

        // 저장할 폴더 및 파일명
        File xlsPath = new File(Environment.getExternalStorageDirectory() + exportFolderName);
        if (! xlsPath.exists())
            xlsPath.mkdirs(); // 디렉토리가 없으면 생성
        xlsFile = new File(xlsPath, outputFileName);
        if (xlsFile.exists()) { // 기존 파일 존재시 삭제
            xlsFile.delete();
        }

        try{
            FileOutputStream os = new FileOutputStream(xlsFile);
            workbook.write(os); // 지정된 외부 저장소에 엑셀 파일 생성
        }catch (IOException e){
            e.printStackTrace();
        }
        Toast.makeText(getApplicationContext(),xlsFile.getAbsolutePath()+"에 저장되었습니다",Toast.LENGTH_SHORT).show();

        if (Build.VERSION.SDK_INT >= 24) { // Android Nougat ( 7.0 ) and later
            Uri uri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".fileprovider",xlsFile);
            Intent intent = new Intent(Intent.ACTION_SEND);
            intent.setDataAndType(uri, "application/excel");
            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
            intent.putExtra(Intent.EXTRA_STREAM,uri);
            startActivity(Intent.createChooser(intent,"엑셀 내보내기"));
        } else {
            Uri uri = Uri.fromFile(xlsFile);
            Intent intent = new Intent(Intent.ACTION_SEND);
            intent.setType("application/excel");
            intent.putExtra(Intent.EXTRA_STREAM,uri);
            startActivity(Intent.createChooser(intent,"엑셀 내보내기"));
        }

        // 미디어 스캐닝
        MediaScannerConnection.scanFile(getApplicationContext(), new String[]{xlsFile.getAbsolutePath()}, null, new MediaScannerConnection.OnScanCompletedListener() {
            @Override
            public void onScanCompleted(String s, Uri uri) {
            }
        });


    }

    private void NotConnected_showAlert() {
        AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this);
        builder.setTitle("네트워크 연결 오류");
        builder.setMessage("사용 가능한 무선네트워크가 없습니다.\n" + "먼저 무선네트워크 연결상태를 확인해 주세요.")
                .setCancelable(false)
                .setPositiveButton("확인", new DialogInterface.OnClickListener() {
                    public void onClick(DialogInterface dialog, int id) {
                        finish(); // exit
                        //application 프로세스를 강제 종료
                        android.os.Process.killProcess(android.os.Process.myPid());
                    }
                });
        AlertDialog alert = builder.create();
        alert.show();
    }

    private boolean NetworkConnection() {
        int[] networkTypes = {ConnectivityManager.TYPE_MOBILE, ConnectivityManager.TYPE_WIFI};
        try {
            ConnectivityManager manager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
            for (int networkType : networkTypes) {
                NetworkInfo activeNetwork = manager.getActiveNetworkInfo();
                if (activeNetwork != null && activeNetwork.getType() == networkType) {
                    return true;
                }
            }
        } catch (Exception e) {
            return false;
        }
        return false;
    }

} 


참조한 블로그 글

http://liveonthekeyboard.tistory.com/148


참조하면 도움되는 Intent 내용

http://gogorchg.tistory.com/entry/Android%ED%8E%8C%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-Intent-%EC%82%AC%EC%9A%A9%EB%B2%95


블로그 이미지

Link2Me

,