2023년 1월 19일 목요일

안드로이드 스튜디오로 웹앱 빠르게 만들기

웹앱 apk 빠르게 만들기...  (feat: 우리는 귀찮은거 시러하니까)
웹사이트 만들어놓고 앱이없냐 이 ㅈㄹ 하면 요로코롬해서 전달해주면됨



준비물:
1. 앱영어이름
2. 앱한글이름
3. 아이콘몬스터(iconmonster)나 오픈클립아트(Openclipart) 같은 무료 이미지사이트를
뒤져서 적절한 앱아이콘 만들어서 png 파일 준비(배경은 투명말고 하얀걸로다가)


[이제시작]


[1. 안드로이드 스튜디오로 새 안드로이드 프로젝트]
만들고 언어는 자바에 empty activity 로 선택하고 앱영어 이름을 넣고 시작한다.


[2. 앱아이콘만든다]
app -> 우클릭해서 new -> image asset 하고 name을 ic_myicon 으로 하고 source asset -> path에서 아까 준비한이미지를 불러오기하고  Finish를 누른다.


[3. http로 접근할수 있으니까 res -> xml -> network_security_config.xml 파일을 새로 만들어서 다음을 기제]

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <base-config cleartextTrafficPermitted="true" />
</network-security-config>


[4. 이미지업로드용 파일제공을위해 res -> xml -> provider_paths.xml 파일을 만들어 다음을 기제]

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


[5. AndroidManifest.xml 파일 세팅하기]

manifest -> 바로 밑에 퍼미션 넣어주기 (파일 다운로드 업로드할수 있으니까)
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.CAMERA2" />
<uses-feature android:name="android.hardware.camera" android:required="true" />


manifest -> application -> android:label  을 한글 앱이름으로 변경


manifest -> application 에서 다음 3줄을 추가
android:networkSecurityConfig="@xml/network_security_config"
android:usesCleartextTraffic="true"
android:requestLegacyExternalStorage="true"


manifest -> application 에서 아까만든 ic_myicon 이미지 Asset으로 변경
android:icon="@mipmap/ic_myicon"
android:roundIcon="@mipmap/ic_myicon_round"


manifest -> application -> provider 에 파일프로바이더 기재
(중간 com. 어쩌고는 자기 프로젝트 아이디로 변경)
<provider
            android:name="androidx.core.content.FileProvider"
            android:authorities="com.example.webappmokup.fileprovider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/provider_paths" />
</provider>


manifest -> application -> activity 에 다음을 기제
(화면 돌렸을때 초기화되면 짜증나니까)
android:configChanges="orientation|keyboardHidden|screenSize"


[6.테마바꾸기]

res -> values -> themes -> themes.xml 파일과 themes.xml (night) 파일에서
<style parent="Theme.MaterialComponents.DayNight.NoActionBar"> 로바꾸고
그밑에 Primary와  Secondary칼라 6개는 전부 블랙으로 @color/black 변경
2개 파일 다 바꿔줘야 다크모드에서 색상이 안바뀜.



[7. MainActivity 레이아웃 바꾸기]

res -> layout -> activiy_main.xml 파일을 열어서 코드보기로 변경후
레이아웃을 RelativeLayout 으로 변경하고 안에 있는 TextView를 지우고 다음으로 교체

<WebView
        android:id="@+id/webview"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
</WebView>



[8. MainActivity.java 파일 변경]

app -> java -> com.*** ->MainActivity.java  파일을 열어서 

package 다음줄을 아래처럼 변경 (그냥 복붙하자 분석은 붙혀놓고 생각)


import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.FileProvider;
import android.Manifest;
import android.app.AlertDialog;
import android.app.DownloadManager;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.os.Parcelable;
import android.provider.MediaStore;
import android.util.Log;
import android.webkit.CookieManager;
import android.webkit.DownloadListener;
import android.webkit.URLUtil;
import android.webkit.ValueCallback;
import android.webkit.WebChromeClient;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.Toast;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;


//롤리팝 이하 버전은 고려안한다..  (지금이 어느세월인데 아직도)
public class MainActivity extends AppCompatActivity {
    private WebView mywebView;
    private String target_url = "https://www.naver.com";

    //이미지파일업로드용
    public ValueCallback<Uri[]> filePathCallbackNormal;
    public final static int FILECHOOSER_REQ_CODE = 2002;
    private String currentPhotoPath;

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

        //권한처리
        checkMyPermissions();

        //
        mywebView=(WebView) findViewById(R.id.webview);
        mywebView.setWebViewClient(new mywebClient());
        mywebView.setWebChromeClient(new CustomChrome(this));           //크롬클라이언트
        mywebView.setDownloadListener(new DownloadListener() {                  //파일다운로드처리
            @Override
            public void onDownloadStart(String url, String userAgent, String contentDisposition, String mimeType, long contentLength) {
                // 파일명 잘라내기 및 확장자 확인
                String fileName = contentDisposition;
                if (fileName != null && fileName.length() > 0) {
                    int idxFileName = fileName.indexOf("filename=");
                    if (idxFileName > -1) {
                        fileName = fileName.substring(idxFileName + 9).trim();
                    }
                    int idxUnderFileName = fileName.indexOf("filename_=UTF-8");
                    if (idxUnderFileName > -1) {
                        fileName = fileName.substring(idxUnderFileName + 15).trim();
                    }
                    if (fileName.endsWith(";")) {
                        fileName = fileName.substring(0, fileName.length() - 1);
                    }
                    if (fileName.startsWith("\"") && fileName.startsWith("\"")) {
                        fileName = fileName.substring(1, fileName.length() - 1);
                    }
                }else{
                    fileName = URLUtil.guessFileName(url, contentDisposition, mimeType);
                }

                DownloadManager.Request request = new DownloadManager.Request(Uri.parse(url));
                request.setMimeType(mimeType);
                String cookies = CookieManager.getInstance().getCookie(url);
                request.addRequestHeader("cookie", cookies);
                request.addRequestHeader("User-Agent", userAgent);
                request.setDescription("Downloading File..");
                request.setTitle(fileName);
                request.allowScanningByMediaScanner();
                request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
                request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName);
                DownloadManager dm = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
                dm.enqueue(request);
                Toast.makeText(getApplicationContext(), "파일다운로드", Toast.LENGTH_LONG).show();
            }
        });

        //
        WebSettings webSettings = mywebView.getSettings();
        webSettings.setJavaScriptEnabled(true);             //자바스크립트허용
        webSettings.setSupportMultipleWindows(false);       //멀티윈도우 안됨
        webSettings.setSupportZoom(true);                   //줌가능
        webSettings.setBuiltInZoomControls(true);           //컨트롤로줌가능
        webSettings.setDisplayZoomControls(false);          //줌컨트롤은없에기
        webSettings.setCacheMode(WebSettings.LOAD_NO_CACHE);  //캐시없이 네트워크로만
        webSettings.setUseWideViewPort(true);               // wideviewport사용
        webSettings.setJavaScriptCanOpenWindowsAutomatically(false); // 자바스크립트가 window.open()사용
        webSettings.setAllowFileAccess(true);               //웹뷰가 파일액세스 활성화
        webSettings.setLoadWithOverviewMode(true);          // 메타태그허용
        webSettings.setDomStorageEnabled(true);             // 로컬저장소허용

        //
        mywebView.loadUrl(target_url);
    }

    //권한처리
    public void checkMyPermissions() {
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
            if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED
                || checkSelfPermission(Manifest.permission.ACCESS_NETWORK_STATE) != PackageManager.PERMISSION_GRANTED
                || checkSelfPermission(Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED
                || checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED
                || checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED)
            {
                //
                String[] permissions = {
                    Manifest.permission.INTERNET,
                    Manifest.permission.ACCESS_NETWORK_STATE,
                    Manifest.permission.CAMERA,
                    Manifest.permission.WRITE_EXTERNAL_STORAGE,
                    Manifest.permission.READ_EXTERNAL_STORAGE
                };
                requestPermissions(permissions, 1);
            }
        }
    }

    //카메라 기능 구현
    public void selectImage() {
        //카메라찍기 인텐트
        Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
        if (takePictureIntent.resolveActivity(getPackageManager()) != null) {
            File photoFile = null;
            try {
                photoFile = createImageFile();
            } catch (IOException ex) {
                Log.e("webappmokup:", "이미지파일을 생성할수 없음", ex);
            }
            //파일이 성공적으로 생성된경우 진행
            if (photoFile != null) {
                //파일프로바이더로 생성된 파일을 가져와야함 -> 엄한짓 하면 안가져옴..  (!!여기서 자기 프로젝트 id로 해야함!!)
                Uri photoURI = FileProvider.getUriForFile(this, "com.example.webappmokup.fileprovider", photoFile);
                takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI);
            } else {
                Toast.makeText(this, "이미지파일 생성안됨", Toast.LENGTH_SHORT).show();
            }
        }

        //선택 인텐트
        Intent pickIntent = new Intent(Intent.ACTION_PICK);
        pickIntent.setType(MediaStore.Images.Media.CONTENT_TYPE);
        pickIntent.setData(MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
        String pickTitle = "이미지를 가져올 방법 선택";
        Intent chooserIntent = Intent.createChooser(pickIntent, pickTitle);

        //takPictureIntent 포함해서 startactivity
        chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, new Parcelable[]{ takePictureIntent });
        startActivityForResult(chooserIntent, FILECHOOSER_REQ_CODE);    //이것도 데프리케이트라고?? 이래서 개짜증나는거야..
    }

    //촬영이미지 파일만들기
    private File createImageFile() throws IOException {
        String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
        String imageFileName = "IMG_" + timeStamp + "_";
        File imageStorageDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES);
        File imageFile = File.createTempFile(
                imageFileName,    /* 파일명 */
                ".jpg",           /* 확장자 */
                imageStorageDir   /* 디렉토리 */
        );

        //
        currentPhotoPath = imageFile.getAbsolutePath();
        return imageFile;
    }

    //웹클라이언트 이너클래스
    public class mywebClient extends WebViewClient  {
        @Override
        public void onPageStarted(WebView view, String url, Bitmap favicon){
            super.onPageStarted(view, url, favicon);
        }
        
        @Override
        public void onPageFinished(WebView view, String url) {
            //쿠키매니저가 종료될때 쿠키허용이 되지 않으면
            //이를 기반으로 하는 로그인처리를 할때 매번 다시 로그인해야함.
            CookieManager.getInstance().setAcceptCookie(true);
            CookieManager.getInstance().acceptCookie();
            CookieManager.getInstance().flush();
        }

        @Override
        public boolean shouldOverrideUrlLoading(WebView view, String url){
            //전화나 메일 링크이동
            if(url.startsWith("tel:")) {
                Intent intent = new Intent(Intent.ACTION_DIAL, Uri.parse(url));
                startActivity(intent);
                return true;
            } else if (url.startsWith("mailto:")) {
                Intent itt = new Intent(Intent.ACTION_SENDTO, Uri.parse(url));
                startActivity(itt);
                return true;
            }

            //
            view.stopLoading();
            view.loadUrl(url);
            return false;
        }
    }

    @Override
    public void onBackPressed() {
        if (mywebView.canGoBack()) {
            mywebView.goBack();
        } else {
            super.onBackPressed();
        }
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
        switch (requestCode)
        {
            case FILECHOOSER_REQ_CODE:
                if (resultCode == RESULT_OK) {
                    if (filePathCallbackNormal == null) { return; }
                    if (data == null) { data = new Intent();  }
                    if (data.getData() == null) { data.setData( Uri.fromFile(new File(currentPhotoPath))); }

                    //콜백처리
                    filePathCallbackNormal.onReceiveValue(WebChromeClient.FileChooserParams.parseResult(resultCode, data));  //파일 바인딩
                    filePathCallbackNormal = null;
                } else {
                    // RESULT_OK 가 아니더라도 콜백클리어
                    if (filePathCallbackNormal != null) {
                        filePathCallbackNormal.onReceiveValue(null);
                        filePathCallbackNormal = null;
                    }
                }
                break;
            default:
                break;
        }

        //
        super.onActivityResult(requestCode, resultCode, data);
    }
}

//유틸추가용 크롬클라이언트 (alert, confirm, filechoose 같은것들)
class CustomChrome extends WebChromeClient {
    private Context mContext;            // WebChromeClient를 호출한 Context
    private AlertDialog mAlertDialog;    // 경고창을 띄울 Dialog
    public CustomChrome(Context context){
        mContext = context;
    }

    @Override
    public boolean onJsAlert(WebView view, String url, String message, final android.webkit.JsResult result){
        if(mAlertDialog == null){
            mAlertDialog = new AlertDialog.Builder(mContext)
                    .setTitle("알림")
                    .setMessage(message)
                    .setPositiveButton(android.R.string.ok, new AlertDialog.OnClickListener() {
                        public void onClick(DialogInterface dialog, int which) {
                            result.confirm();
                            mAlertDialog.dismiss();
                            mAlertDialog = null;
                        }
                    })
                    .setCancelable(false)
                    .create();
        }

        mAlertDialog.show();
        return true;
    }

    @Override
    public boolean onJsConfirm(final WebView view,final String url, final String message, final android.webkit.JsResult result) {
        if(mAlertDialog == null){
            mAlertDialog = new AlertDialog.Builder(mContext)
                    .setTitle("확인")
                    .setMessage(message)
                    .setPositiveButton(android.R.string.ok, new AlertDialog.OnClickListener() {
                        public void onClick(DialogInterface dialog, int which) {
                            result.confirm();
                            mAlertDialog.dismiss();
                            mAlertDialog = null;
                        }
                    })
                    .setNegativeButton(android.R.string.cancel,new DialogInterface.OnClickListener(){
                        public void onClick(DialogInterface dialog, int which) {
                            result.cancel();
                            mAlertDialog.dismiss();
                            mAlertDialog = null;
                        }
                    })
                    .setCancelable(false)
                    .create();
        }
        mAlertDialog.show();
        return true;
    }

    @Override
    public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams) {
        MainActivity mainact = (MainActivity) mContext;

        //Callback 초기화
        if (mainact.filePathCallbackNormal != null) {
            mainact.filePathCallbackNormal.onReceiveValue(null);
            mainact.filePathCallbackNormal = null;
        }

        //콜백 다시바인딩
        mainact.filePathCallbackNormal = filePathCallback;
        //이미지 선택
        mainact.selectImage();
        return true;
    }
}

[!!! 당연히 target_url 에 해당하는 도메인부분과 FileProvider의 Uri의 id는 
자기껄로 바꺼야함 !!!]


[9. Gradle Scripts의 app 수준의 build.gradle 에서]

compileSdk  와 targetSdk를 33  으로 바꾼다.


[10. 메뉴 -> Build -> Generate Signed Bundle / APK 로 가서]

APK를 선택하고  key파일이 없으면 새로 생성하고 있으면 선택하여 비번넣고
Next눌러서 release로 선택하고 Finish 하면 대상폴더에 apk 파일 생성됨


이렇게 만들어진 apk파일을 전달해주면 끝!!

물론 아이폰은 안된다고 하고... -_-

(근데 유럽에서 설치파일되게 해달라고 ㅈㄹ해서 조만간 소식있을듯?)


혹시몰라 유튜브 링크 걸어놓음

How To Create WebView App In Android Studio - YouTube


그리고 압축파일도 올려놓음

예제파일 다운로드


코틀린으로 할껄그랬나.. -_-;

하여튼 자바/코틀린 왔다갔다 짜증남..

이게다.. 미친 오라클 때문이다..

댓글 없음:

댓글 쓰기