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