보호되어 있는 글입니다.
내용을 보시려면 비밀번호를 입력하세요.

728x90

https://link2me.tistory.com/1506 에 있는 파일 다운로드 및 실행 코드와 원리는 동일한 것도 포함되어 있으니 참고하면 도움된다.

 

전체 소스코드는 GitHUB 에 올려진 것을 참조하시라.

서버 코드와 관련된 URL, AES Key는 변경해서 올렸다.

https://github.com/jsk005/JavaProjects/tree/master/pdfviewer

 

GitHub - jsk005/JavaProjects: 자바 기능 테스트

자바 기능 테스트. Contribute to jsk005/JavaProjects development by creating an account on GitHub.

github.com

 

AndroidManifest.xml

<?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.link2me.android.pdfviewer">
 
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
    <uses-permission
        android:name="android.permission.REQUEST_INSTALL_PACKAGES"
        tools:ignore="ProtectedPermissions" />
    <uses-permission android:name="android.permission.READ_PHONE_STATE" />
    <uses-permission android:name="android.permission.READ_PHONE_NUMBERS" />
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.WRITE_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"
        android:usesCleartextTraffic="true">
        <activity
            android:name=".ui.MainActivity"
            android:exported="false" />
        <activity
            android:name=".ui.SplashActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
 
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
 
        <provider
            android:name="androidx.core.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>
 

 

 

 

package com.link2me.android.pdfviewer.ui;
 
import android.content.Context;
import android.content.Intent;
import android.media.MediaScannerConnection;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.view.View;
import android.widget.Toast;
 
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.FileProvider;
import androidx.recyclerview.widget.DividerItemDecoration;
import androidx.recyclerview.widget.LinearLayoutManager;
 
import com.link2me.android.common.BackPressHandler;
import com.link2me.android.common.Utils;
import com.link2me.android.common.Value;
import com.link2me.android.pdfviewer.R;
import com.link2me.android.pdfviewer.adapter.BindPdfViewListAdapter;
import com.link2me.android.pdfviewer.databinding.ActivityMainBinding;
import com.link2me.android.pdfviewer.model.PdfResult;
import com.link2me.android.pdfviewer.model.Pdf_Item;
import com.link2me.android.pdfviewer.network.RetrofitAdapter;
import com.link2me.android.pdfviewer.network.RetrofitService;
import com.link2me.android.pdfviewer.network.RetrofitUrl;
 
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
 
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
 
public class MainActivity extends AppCompatActivity implements BindPdfViewListAdapter.OnItemClickListener {
    private final String TAG = this.getClass().getSimpleName();
    Context mContext;
 
    private ActivityMainBinding binding;
 
    private ArrayList<Pdf_Item> pdfItemList = new ArrayList<>();
    private BindPdfViewListAdapter mAdapter;
 
    DownloadPdfFromURL downloadApk;
    private File outputFile;
 
    private BackPressHandler backPressHandler;
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        binding = ActivityMainBinding.inflate(getLayoutInflater());
        View view = binding.getRoot();
        setContentView(view);
 
        backPressHandler = new BackPressHandler(this); // 뒤로 가기 버튼 이벤트
        mContext = MainActivity.this;
        initView();
    }
 
    private void initView() {
        createPdfList(); // 서버 데이터 가져오기
        buildRecyclerView();
    }
 
    private void createPdfList() {
        String keyword = Value.encryptAES(Value.URLkey());
        RetrofitService service = RetrofitAdapter.getClient().create(RetrofitService.class);
        Call<PdfResult> call = service.getPdfData(keyword,"");
        call.enqueue(new Callback<PdfResult>() {
            @Override
            public void onResponse(Call<PdfResult> call, Response<PdfResult> response) {
                if(response.body().getStatus().contains("success")){
                    pdfItemList.clear(); // 서버에서 가져온 데이터 초기화
 
                    for(Pdf_Item item: response.body().getPdfinfo()){
                        pdfItemList.add(item);
                    }
 
                    // runOnUiThread()를 호출하여 실시간 갱신한다.
                    runOnUiThread(() -> {
                        // 갱신된 데이터 내역을 어댑터에 알려줌
                        mAdapter.notifyDataSetChanged();
                    });
                } else {
                    Utils.showAlert(mContext,response.body().getStatus(),response.body().getMessage());
                }
            }
 
            @Override
            public void onFailure(Call<PdfResult> call, Throwable t) {
 
            }
        });
    }
 
    private void buildRecyclerView(){
        binding.pdfListview.setHasFixedSize(true);
        LinearLayoutManager manager = new LinearLayoutManager(mContext);
        mAdapter = new BindPdfViewListAdapter(mContext,pdfItemList); // 객체 생성
 
        DividerItemDecoration decoration = new DividerItemDecoration(mContext,manager.getOrientation());
        binding.pdfListview.addItemDecoration(decoration);
        binding.pdfListview.setLayoutManager(manager);
        binding.pdfListview.setAdapter(mAdapter);
 
        mAdapter.setOnItemSelectClickListener(this);
    }
 
    @Override
    public void onItemClicked(View view, Pdf_Item item, int position) {
//        Log.d(TAG, RetrofitUrl.BASE_URL+item.getPdfurl());
        Toast.makeText(getApplicationContext(), "잠시 기다리시면 "+item.getTitle()+" PDF 파일 열람이 가능합니다", Toast.LENGTH_LONG).show();
        String PDFUrl = RetrofitUrl.BASE_URL+item.getPdfurl();
        downloadPDF(PDFUrl);
    }
 
    private void downloadPDF(String fileUrl) {
        // 백그라운드 객체를 만들어 주어야 다운로드 취소가 제대로 동작됨
        downloadApk = new DownloadPdfFromURL();
        downloadApk.execute(fileUrl);
    }
 
    class DownloadPdfFromURL extends AsyncTask<String, Integer, String> {
 
        @Override
        protected String doInBackground(String... strings) {
            int count;
            int lenghtOfFile = 0;
            InputStream input = null;
            OutputStream fos = null;
 
            File filePath = new File(Environment.getExternalStorageDirectory() + "/download");
            outputFile = new File(filePath, "tempPDF.pdf");
            if (outputFile.exists()) { // 기존 파일 존재시 삭제하고 다운로드
                outputFile.delete();
            }
 
            try {
                URL url = new URL(strings[0]);
                URLConnection connection = url.openConnection();
                connection.connect();
 
                lenghtOfFile = connection.getContentLength(); // 파일 크기를 가져옴
 
                input = new BufferedInputStream(url.openStream());
                fos = new FileOutputStream(outputFile);
                byte data[] = new byte[1024];
                long total = 0;
 
                while ((count = input.read(data)) != -1) {
                    if (isCancelled()) {
                        input.close();
                    }
                    total = total + count;
                    if (lenghtOfFile > 0) { // 파일 총 크기가 0 보다 크면
                        publishProgress((int) (total * 100 / lenghtOfFile));
                    }
                    fos.write(data, 0, count); // 파일에 데이터를 기록
                }
 
                fos.flush();
 
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                if (input != null) {
                    try {
                        input.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
                if (fos != null) {
                    try {
                        fos.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
            return null;
        }
 
        protected void onPostExecute(String result) {
            if (result == null) {
                // 미디어 스캐닝
                MediaScannerConnection.scanFile(getApplicationContext(), new String[]{outputFile.getAbsolutePath()}, nullnew MediaScannerConnection.OnScanCompletedListener() {
                    @Override
                    public void onScanCompleted(String s, Uri uri) {
 
                    }
                });
 
                // 다운로드한 파일 실행하여 업그레이드 진행하는 코드
                if (Build.VERSION.SDK_INT >= 24) {
                    openPDF(outputFile);
                } else {
                    Intent intent = new Intent(Intent.ACTION_VIEW);
                    Uri apkUri = Uri.fromFile(outputFile);
                    intent.setDataAndType(apkUri, "application/pdf");
                    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                    getApplicationContext().startActivity(intent);
                }
 
            } else {
                Toast.makeText(getApplicationContext(), "다운로드 에러", Toast.LENGTH_LONG).show();
            }
        }
    }
 
    void openPDF(File file) {
        Uri fileUri = FileProvider.getUriForFile(mContext, mContext.getApplicationContext().getPackageName() + ".fileprovider",file);
 
        Intent intent = new Intent(Intent.ACTION_VIEW);
        intent.setDataAndType(fileUri, "application/pdf");
        intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
        intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
        try {
            startActivity(intent);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
 
    @Override
    public void onBackPressed() {
        backPressHandler.onBackPressed();
    }
}
 

 

 

'안드로이드 > Android 활용' 카테고리의 다른 글

Android APP(Java)에서 PHP Session 저장 및 사용  (0) 2023.12.23
WebView assets html 파일 읽기  (0) 2020.11.27
Android TextToSpeech  (0) 2020.09.30
Intent 이메일 전송하기  (0) 2020.09.08
Profile 이미지 처리  (0) 2020.09.01
블로그 이미지

Link2Me

,
728x90

보통 WebView는 서버에 있는 html 파일이나 PHP 등의 파일을 읽어서 보여주는 기능을 한다.

License.html 와 같은 파일은 앱에 직접 경로를 지정하여 읽어들이면 좋다.




아래와 같이 assets 폴더가 생성되었다.

이제 여기에 license.html 파일을 복사해서 넣는다.

물론 html 파일 하나로 css, javascript, html 모두 포함되도록 작성하는 게 좋다.

분리해서 파일을 작성할 경우에는 같은 폴더에 파일이 존재해야 한다.



WebView 에서 경로 지정은 어떻게 해야 할까?

아래 색상 표시한 것이 assets 폴더를 인식하는 URL 이다.

WebView webView = findViewById(R.id.webView);
webView.loadUrl("file:///android_asset/license.html"); 


'안드로이드 > Android 활용' 카테고리의 다른 글

Android APP(Java)에서 PHP Session 저장 및 사용  (0) 2023.12.23
PDF Viewer 구현 예제(Java Code)  (0) 2022.06.05
Android TextToSpeech  (0) 2020.09.30
Intent 이메일 전송하기  (0) 2020.09.08
Profile 이미지 처리  (0) 2020.09.01
블로그 이미지

Link2Me

,
728x90

Android Text 를 Speech 로 변환하는 예제다.


<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".TextToSpeechActivity">

    <com.google.android.material.appbar.AppBarLayout
        android:id="@+id/layout_appbar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:theme="@style/AppTheme.AppBarOverlay"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">

        <androidx.appcompat.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:background="?attr/colorPrimary"
            app:popupTheme="@style/AppTheme.PopupOverlay" />

    </com.google.android.material.appbar.AppBarLayout>

    <com.google.android.material.textfield.TextInputLayout
        android:id="@+id/layout_textvoice"
        style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginStart="24dp"
        android:layout_marginTop="30dp"
        android:layout_marginEnd="24dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/layout_appbar">

        <com.google.android.material.textfield.TextInputEditText
            android:id="@+id/et_text2voice"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="텍스트를 입력하세요"
            android:padding="10dp"></com.google.android.material.textfield.TextInputEditText>

    </com.google.android.material.textfield.TextInputLayout>

    <com.google.android.material.button.MaterialButton
        android:id="@+id/btn_speech"
        style="@style/Widget.AppCompat.Button.Borderless.Colored"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginStart="24dp"
        android:layout_marginTop="30dp"
        android:layout_marginEnd="24dp"
        android:padding="10dp"
        android:text="음성변환"
        android:textColor="@color/colorOrangeDark"
        android:textSize="16sp"
        android:textStyle="bold"
        app:backgroundTint="@color/colorSkyBlue"
        app:cornerRadius="15dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/layout_textvoice" />

</androidx.constraintlayout.widget.ConstraintLayout>
 



import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.speech.tts.TextToSpeech;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;

import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;

import java.util.Locale;

public class TextToSpeechActivity extends AppCompatActivity {
    private final String TAG = this.getClass().getSimpleName();
    Context mContext;

    private TextToSpeech textToSpeech;
    private EditText speakText;
    private Button speakBtn;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_speech);
        mContext = TextToSpeechActivity.this;

        initView();
    }

    private void initView() {
        Toolbar toolbar = findViewById(R.id.toolbar);
        toolbar.setTitle("TextToSpeech");
        setSupportActionBar(toolbar);

        speakText = findViewById(R.id.et_text2voice);
        speakBtn = findViewById(R.id.btn_speech);

        textToSpeech = new TextToSpeech(getApplicationContext(), status -> {
            if (status != TextToSpeech.ERROR) {
                textToSpeech.setLanguage(Locale.KOREAN);
            }
        });

        speakBtn.setOnClickListener(v -> texttoSpeak());
    }

    private void texttoSpeak() {
        String text = speakText.getText().toString();
        if ("".equals(text)) {
            text = "Please enter some text to speak.";
            Toast.makeText(getApplicationContext(), text, Toast.LENGTH_SHORT).show();
            return;
        }

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            textToSpeech.speak(text, TextToSpeech.QUEUE_FLUSH, null, null);
        } else {
            String utteranceId = this.hashCode() + "";
            textToSpeech.speak(text, TextToSpeech.QUEUE_FLUSH, null, utteranceId);
        }
    }

    @Override
    protected void onDestroy() {
        if (textToSpeech != null) {
            textToSpeech.stop();
            textToSpeech.shutdown();
        }
        super.onDestroy();
    }

    @Override
    public void onBackPressed() {
        super.onBackPressed();
        startActivity(new Intent(mContext,SplashActivity.class));
        finish();
    }
}


예제 전체 소스코드는 https://github.com/jsk005/JavaProjects/tree/master/speechtext 에 있다.

블로그 이미지

Link2Me

,
728x90

Intent 를 이용한 메일 발송 함수 이다.

구글 메일에서도 잘 보내지고, 기본 메일에서도 잘 보내진다.


테스트 환경 : 삼성 갤럭스 노트9


private void sendEmail_default(String emailTo, String subject, String message){
    Intent emailSelectorIntent = new Intent( Intent.ACTION_SENDTO );
    emailSelectorIntent.setData( Uri.parse( "mailto:" ) );

    final Intent emailIntent = new Intent( Intent.ACTION_SEND );
    emailIntent.putExtra( Intent.EXTRA_EMAIL, new String[]{ emailTo } );
    emailIntent.putExtra(Intent.EXTRA_SUBJECT, subject);
    emailIntent.putExtra(Intent.EXTRA_TEXT, message);
    emailIntent.addFlags( Intent.FLAG_GRANT_READ_URI_PERMISSION );
    emailIntent.addFlags( Intent.FLAG_GRANT_WRITE_URI_PERMISSION );
    emailIntent.setSelector( emailSelectorIntent );

    startActivity( emailIntent );


}


블로그 이미지

Link2Me

,
728x90

회원 로그인 후 회원 정보를 가져와서 ProfileView 함수에 처리하는 로직이다.


private void ProfileView() {
    ImageView img_profile = findViewById(R.id.img_profile);
    TextView userNM = findViewById(R.id.tv_userNM);
    TextView mobileNO = findViewById(R.id.tv_mobileNO);

    String photoURL = Value.PhotoADDRESS + PrefsHelper.read("profileImg","") + ".jpg";
    // 사진 이미지가 존재하지 않을 수도 있으므로 존재 여부를 체크하여 존재하면 ImageView 에 표시한다.
    PhotoURLExists task = new PhotoURLExists();
    try {
        if(task.execute(photoURL).get()==true){
            Glide.with(mContext).load(photoURL).override(170, 200).into(img_profile);
        }
    } catch (ExecutionException e) {
        e.printStackTrace();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

    userNM.setText(PrefsHelper.read("userNM",""));
    mobileNO.setText(PrefsHelper.read("mobileNO",""));
}

private class PhotoURLExists extends AsyncTask<String, Void, Boolean> {
    @Override
    protected Boolean doInBackground(String... params) {
        try {
            HttpURLConnection.setFollowRedirects(false);
            HttpURLConnection con =  (HttpURLConnection) new URL(params[0]).openConnection();
            con.setRequestMethod("HEAD");
            return (con.getResponseCode() == HttpURLConnection.HTTP_OK);
        }
        catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
} 


앱 build.gradle 추가

implementation 'com.github.bumptech.glide:glide:4.11.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0' 


Glide.with(this)
    .load("이미지 url...")
    .override(이미지 사이즈) // ex) override(170, 200)
    .into(imageView);


Glide 최신버전 확인 : https://github.com/bumptech/glide


참고하면 도움되는 자료

AsyncTask return 결과 처리하는 방법 : https://link2me.tistory.com/1516

[Java] SharedPreferences Singleton

출처: https://link2me.tistory.com/1827 [소소한 일상 및 업무TIP 다루기]
[Java] SharedPreferences Singleton

출처: https://link2me.tistory.com/1827 [소소한 일상 및 업무TIP 다루기]
[Java] SharedPreferences Singleton

출처: https://link2me.tistory.com/1827 [소소한 일상 및 업무TIP 다루기]

[Java] SharedPreferences Singletonhttps://link2me.tistory.com/1827


블로그 이미지

Link2Me

,
728x90

Last Updated 2020.6.26


테스트 환경 : 삼성 갤럭시노트9 (AndroidQ)


안드로이드폰의 운영체제를 업그레이드하여 8.0 이다. (9.0 이상 사용폰은 마지막에 수정된 부분만 별도 언급)

API 23까지는 APK 파일을 서버에서 다운로드받고 자동으로 설치하는 화면을 띄우는 코드는

Intent intent = new Intent(Intent.ACTION_VIEW);
 Uri apkUri = Uri.fromFile(outputFile);
 intent.setDataAndType(apkUri, "application/vnd.android.package-archive");
 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
 context.startActivity(intent);


Android 7.0(Nougat) 이상에서는 위 코드로 실행하면 에러가 발생한다.

앱 외부에서 file:// URI의 노출을 금지하는 StrictMode API 정책을 적용한다.
파일 URI를 포함하는 인텐트가 앱을 떠나면 FileUriExposedException 예외와 함께 앱에 오류가 발생한다.


애플리케이션 간에 파일을 공유하려면 content:// URI를 보내고 이 URI에 대해 임시 액세스 권한을 부여해야 한다.
FileProvider 그 권한을 가장 쉽게 부여하는 방법이다.


구글링해서 찾은 대부분의 자료들이 개념 중심으로 적혀 있어 내용 파악이 쉽지 않더라. 테스트한 코드 핵심 내용을 모두 적었다.


provider_paths.xml

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


<files-path name="files" path="." />


File path = getFilesDir();
File file = new File(path, "abc.apk");


파일 경로를 변경해서 테스트 해보니 마찬가지로 잘 동작된다.

https://stackoverflow.com/questions/37074872/android-fileprovider-on-custom-external-storage-folder 참조


<external-path name="download" path="." />

File path = new File(Environment.getExternalStorageDirectory() + "/download");



AndroidManifest.xml

<?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.link2me.filedownload">

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

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

    <application
        android:allowBackup="false"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme.NoActionBar"

        android:usesCleartextTraffic="true">
        <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="androidx.core.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>

        <activity android:name=".DownloadAPK" />
    </application>

</manifest>



MainActivity.java

import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;

import com.gun0912.tedpermission.PermissionListener;
import com.gun0912.tedpermission.TedPermission;

import java.util.ArrayList;

public class MainActivity extends AppCompatActivity {
    Context context;

    PermissionListener permissionlistener = new PermissionListener() {
        @Override
        public void onPermissionGranted() {
            MainActivity.this.Button_Click();
        }

        @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();

        if (Build.VERSION.SDK_INT >= 23) {
            TedPermission.with(this)
                    .setPermissionListener(permissionlistener)
                    .setRationaleMessage("파일을 다운로드 하기 위해서는 접근 권한이 필요합니다")
                    .setDeniedMessage("앱에서 요구하는 권한설정이 필요합니다...\n [설정] > [권한] 에서 사용으로 활성화해주세요.")
                    .setPermissions(new String[]{"android.permission.WRITE_EXTERNAL_STORAGE", "android.permission.READ_EXTERNAL_STORAGE"})
                    .check();
        } else {
            Button_Click();
        }

    }

    private void Button_Click() {
        Button btn_dnload = (Button) findViewById(R.id.btn_image_download);
        btn_dnload.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Intent intent = new Intent(MainActivity.this, DownloadAPK.class);
                startActivity(intent);
            }
        });
    }
}


Value.java

public class Value extends Activity {
    public static final String APKNAME = "ABC.apk"; // APK name
    public static final String IPADDRESS = "http://100.100.100.100";
}



DownloadAPK.java

import android.content.Context;
import android.content.Intent;
import android.media.MediaScannerConnection;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.support.v4.content.FileProvider;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.net.URLConnection;

public class DownloadAPK extends AppCompatActivity {
    Context context;
    private File outputFile;
    ProgressBar progressBar;
    TextView textView;
    LinearLayout linearLayout;
    DownloadFileFromURL downloadFileAsyncTask;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.downloadapk_activity);
        context = this.getBaseContext();

        linearLayout = (LinearLayout) findViewById(R.id.downloadprogress_layout);
        textView = (TextView) findViewById(R.id.txtView01);
        progressBar = (ProgressBar) findViewById(R.id.progressBar);

        DownloadAPK();
    }

    private void DownloadAPK() {
        // 백그라운드 객체를 만들어 주어야 다운로드 취소가 제대로 동작됨
        downloadFileAsyncTask = new DownloadFileFromURL();
        downloadFileAsyncTask.execute(Value.IPADDRESS + "/Download.php");
    }

    class DownloadFileFromURL extends AsyncTask<String, Integer, String> {

        @Override
        protected void onPreExecute() {
            super.onPreExecute();
            progressBar.setProgress(0);
        }

        @Override
        protected String doInBackground(String... apkurl) {
            int count;
            int lenghtOfFile = 0;
            InputStream input = null;
            OutputStream fos = null;

            try {
                URL url = new URL(apkurl[0]);
                URLConnection connection = url.openConnection();
                connection.connect();

                lenghtOfFile = connection.getContentLength(); // 파일 크기를 가져옴

                File path = getFilesDir();
                outputFile = new File(path, Value.APKNAME);
                if (outputFile.exists()) { // 기존 파일 존재시 삭제하고 다운로드
                    outputFile.delete();
                }

                input = new BufferedInputStream(url.openStream());
                fos = new FileOutputStream(outputFile);
                byte data[] = new byte[1024];
                long total = 0;

                while ((count = input.read(data)) != -1) {
                    if (isCancelled()) {
                        input.close();
                        return String.valueOf(-1);
                    }
                    total = total + count;
                    if (lenghtOfFile > 0) { // 파일 총 크기가 0 보다 크면
                        publishProgress((int) (total * 100 / lenghtOfFile));
                    }
                    fos.write(data, 0, count); // 파일에 데이터를 기록
                }

                fos.flush();

            } catch (Exception e) {
                e.printStackTrace();
                Log.e("UpdateAPP", "Update error! " + e.getMessage());
            } finally {
                if (input != null) {
                    try {
                        input.close();
                    }
                    catch(IOException ioex) {
                    }
                }
                if (fos != null) {
                    try {
                        fos.close();
                    }
                    catch(IOException ioex) {
                    }
                }
            }
            return null;
        }

        protected void onProgressUpdate(Integer... progress) {
            super.onProgressUpdate(progress);
            // 백그라운드 작업의 진행상태를 표시하기 위해서 호출하는 메소드
            progressBar.setProgress(progress[0]);
            textView.setText("다운로드 : " + progress[0] + "%");
        }

        protected void onPostExecute(String result) {
            if (result == null) {
                progressBar.setProgress(0);
                Toast.makeText(getApplicationContext(), "다운로드 완료되었습니다.", Toast.LENGTH_LONG).show();

                System.out.println("getPackageName : "+getPackageName());
                System.out.println("APPLICATION_ID Path : "+BuildConfig.APPLICATION_ID);
                System.out.println("outputFile Path : "+ outputFile.getAbsolutePath());
                System.out.println("Fie getPath : "+ outputFile.getPath());

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

                    }
                });


                // 다운로드한 파일 실행하여 업그레이드 진행하는 코드
                if (Build.VERSION.SDK_INT >= 24) {
                    // Android Nougat ( 7.0 ) and later
                    installApk(outputFile);
                    System.out.println("SDK_INT 24 이상 ");
                } else {
                    Intent intent = new Intent(Intent.ACTION_VIEW);
                    Uri apkUri = Uri.fromFile(outputFile);
                    intent.setDataAndType(apkUri, "application/vnd.android.package-archive");
                    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                    getApplicationContext().startActivity(intent);
                    System.out.println("SDK_INT 23 이하 ");
                }

            } else {
                Toast.makeText(getApplicationContext(), "다운로드 에러", Toast.LENGTH_LONG).show();
            }
        }

        protected void onCancelled() {
            // cancel메소드를 호출하면 자동으로 호출되는 메소드
            progressBar.setProgress(0);
            textView.setText("다운로드 진행 취소됨");
        }
    }

    public void installApk(File file) {
        Uri uri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".fileprovider",file);
        Intent intent = new Intent(Intent.ACTION_VIEW);
        intent.setDataAndType(uri, "application/vnd.android.package-archive");
        intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
        context.startActivity(intent);
    }

}

위 분홍색 installApk() 메서드는 에러가 발생할 것이다.

BuildConfig.APPLICATION_ID 를 제대로 인식하지 못하는 문제더라.


    public void installApk(File file) {
        Uri fileUri = FileProvider.getUriForFile(context, context.getApplicationContext().getPackageName() + ".fileprovider",file);
        Intent intent = new Intent(Intent.ACTION_VIEW);
        intent.setDataAndType(fileUri, "application/vnd.android.package-archive");
        intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
        intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
        startActivity(intent);
        finish();
    } 

이 코드로 대체하면 정상적으로 앱 업데이트가 이루어진다.


앱 build.gradle

apply plugin: 'com.android.application'
// 코틀린 혼용 사용을 위해 추가
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'

android {
    compileSdkVersion 29
    buildToolsVersion "29.0.3"

    defaultConfig {
        applicationId "com.link2me.android.abc"
        minSdkVersion 23
        targetSdkVersion 29
        versionCode 1
        versionName "1.0"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility = 1.8
        targetCompatibility = 1.8
    }

}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" // 코틀린 혼용 사용을 위해 추가
    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'androidx.core:core-ktx:1.2.0' // 코틀린 혼용 사용을 위해 추가
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'

    implementation 'com.journeyapps:zxing-android-embedded:3.6.0'
    implementation 'gun0912.ted:tedpermission:2.0.0'
    implementation 'com.google.android.material:material:1.0.0'

    implementation 'com.naver.maps:map-sdk:3.7.1' // 네이버 지도 SDK
    implementation 'com.google.android.gms:play-services-location:17.0.0'
}


안드로이드 8.0 출처를 알 수 없는 앱 설정화면 코드는 http://mixup.tistory.com/78 참조해서 해결했다.


Value.APKNAME 은 "abc.apk" 와 같은 명칭이다.
미디어 스캐닝은 파일 다운로드한 폴더의 내용을 확인할 목적으로 검색하다 알게된 걸 적용해봤다.


참고 사이트

https://pupli.net/2017/06/18/install-apk-files-programmatically/


http://stickyny.tistory.com/110


http://mixup.tistory.com/98


도움이 되셨다면 광고 클릭 해 주세요. 좋은 글 작성에 큰 힘이 됩니다

출처: https://link2me.tistory.com/1110 [소소한 일상 및 업무TIP 다루기]

도움이 되셨다면 댓글 달아주세요. 좋은 글 작성에 큰 힘이 됩니다.

블로그 이미지

Link2Me

,
728x90

안드로이드에서 Java Singleton 패턴으로 SharedPreferences 처리를 하는 코드를 적어둔다.


import android.content.Context;
import android.content.SharedPreferences;

public class PrefsHelper {
    private static Context mContext;
    private static SharedPreferences prefs;
    private static SharedPreferences.Editor editor;
    private static PrefsHelper prefmodule = null;
    public static final String PREF_NAME ="pref";

    public static final String BaudRate = "BaudRate";
    public static final String DataBit = "DataBit";
    public static final String StopBit = "StopBit";
    public static final String Parity ="Parity";

    public static PrefsHelper getInstance(Context context) {
        mContext = context;

        if (prefmodule == null) {
            prefmodule = new PrefsHelper();
        }
        if(prefs==null){
            prefs = mContext.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
            editor = prefs.edit();
        }
        return prefmodule;
    }

    public static String read(String key, String defValue) {
        return prefs.getString(key, defValue);
    }

    public static void write(String key, String value) {
        SharedPreferences.Editor prefsEditor = prefs.edit();
        prefsEditor.putString(key, value);
        prefsEditor.commit();
    }

    public static boolean read(String key, boolean defValue) {
        return prefs.getBoolean(key, defValue);
    }

    public static void write(String key, boolean value) {
        SharedPreferences.Editor prefsEditor = prefs.edit();
        prefsEditor.putBoolean(key, value);
        prefsEditor.commit();
    }

    public static Integer read(String key, int defValue) {
        return prefs.getInt(key, defValue);
    }

    public static void write(String key, Integer value) {
        SharedPreferences.Editor prefsEditor = prefs.edit();
        prefsEditor.putInt(key, value).commit();
    }
}


사용법 write

private void writePreferences(String baudRate, String dataBit, String stopBit, int parity){
    PrefsHelper.getInstance(this).write(PrefsHelper.BaudRate, baudRate);
    PrefsHelper.getInstance(this).write(PrefsHelper.DataBit, dataBit);
    PrefsHelper.getInstance(this).write(PrefsHelper.StopBit, stopBit);
    PrefsHelper.getInstance(this).write(PrefsHelper.Parity, String.valueOf(parity));
}


읽기

String BaudRate = PrefsHelper.getInstance(mContext).read(PrefsHelper.BaudRate,null);
String DataBit = PrefsHelper.getInstance(mContext).read(PrefsHelper.DataBit,null);
String StopBit = PrefsHelper.getInstance(mContext).read(PrefsHelper.StopBit,null);
String Parity = PrefsHelper.getInstance(mContext).read(PrefsHelper.Parity,null);




다른 예제

이 예제를 활용하면 여러모로 편하다.

import android.content.Context;
import android.content.SharedPreferences;

public class PrefsHelper {
    public static final String PREFERENCE_NAME="pref";
    private Context mContext;
    private static SharedPreferences prefs;
    private static SharedPreferences.Editor prefsEditor;
    private static PrefsHelper instance;

    public static synchronized PrefsHelper init(Context context){
        if(instance == null)
            instance = new PrefsHelper(context);
        return instance;
    }

    private PrefsHelper(Context context) {
        mContext = context;
        prefs = mContext.getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE );
        prefsEditor = prefs.edit();
    }

    public static String read(String key, String defValue) {
        return prefs.getString(key, defValue);
    }

    public static void write(String key, String value) {
        prefsEditor.putString(key, value);
        prefsEditor.commit();
    }

    public static Integer read(String key, int defValue) {
        return prefs.getInt(key, defValue);
    }

    public static void write(String key, Integer value) {
        prefsEditor.putInt(key, value).commit();
    }

    public static boolean read(String key, boolean defValue) {
        return prefs.getBoolean(key, defValue);
    }

    public static void write(String key, boolean value) {
        prefsEditor.putBoolean(key, value);
        prefsEditor.commit();
    }
}


사용방법

PrefsHelper.init(getApplicationContext()); // 한번만 실행하면 된다.


// 쓰기

PrefsHelper.write("userid",userID);
PrefsHelper.write("userpw",userPW);


// 읽기

userID = PrefsHelper.read("userid","");
userPW = PrefsHelper.read("userpw","");

블로그 이미지

Link2Me

,
728x90

Material Design Navigation Drawer


Material Design 에 대한 자료는 https://github.com/material-components/material-components-android 를 참조하면 도움된다.

안드로이드 네비게이션은 왼쪽에 슬라이드 형식으로 메뉴가 나왔다 들어갔다 하는 패널을 말한다.


앱 build.gradle 추가 사항

implementation 'com.google.android.material:material:1.1.0'


먼저 Material Design 사이트에서 제공하는 예제 파일을 열어봤더니 아래와 같은 예제가 나온다.

자식 View는 반드시 2개만 가진다는 점을 기억하자.

순서는 content 자식 View가 먼저 나오고 drawer View는 나중에 나온다.

그러면 drawer View가 나중에 나오기만 하면 되는 것인가? 아니다.


<?xml version="1.0" encoding="utf-8"?>
<androidx.drawerlayout.widget.DrawerLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/drawer_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true">
    <!-- As the main content view, the view below consumes the entire
         space available using match_parent in both dimensions. Note that
         this child does not specify android:layout_gravity attribute. -->
    <FrameLayout
        android:id="@+id/content"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <!-- android:layout_gravity="start" tells DrawerLayout to treat
         this as a sliding drawer on the starting side, which is
         left for left-to-right locales. The navigation view extends
         the full height of the container. A
         solid background is used for contrast with the content view.
         android:fitsSystemWindows="true" tells the system to have
         DrawerLayout span the full height of the screen, including the
         system status bar on Lollipop+ versions of the plaform. -->
    <com.google.android.material.navigation.NavigationView
        android:id="@+id/nav_view"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:layout_gravity="start"
        android:background="#333"
        android:fitsSystemWindows="true"
        app:menu="@menu/navigation_view_content"
        app:itemIconTint="@color/emerald_translucent"
        app:itemTextColor="@color/emerald_text"
        app:itemBackground="@color/sand_default"
        app:itemTextAppearance="@style/TextMediumStyle" />

</androidx.drawerlayout.widget.DrawerLayout>


위와 같이 하거나 또는

<?xml version="1.0" encoding="utf-8"?>
<androidx.drawerlayout.widget.DrawerLayout 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/drawer_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    tools:openDrawer="start">

    <!-- 1. 콘텐츠 영역-->
    <include
        layout="@layout/content_main"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <!-- 2. 왼쪽 사이드 메뉴-->
    <include layout="@layout/nav_header_main" />   

</androidx.drawerlayout.widget.DrawerLayout>


아래 nav_header_main.xml 파일을 보면 com.google.android.material.navigation.NavigationView 대신에 LinearLayout 으로 되어 있다. 꼭 com.google.android.material.navigation.NavigationView 를 사용하지 않아도 된다는 의미다.

android:layout_gravity="start" 를 추가해서 왼쪽 슬라이드 메뉴로 사용하겠다는 것만 포함되어 있다.


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/navi_view"
    android:layout_width="280dp"
    android:layout_height="match_parent"
    android:layout_gravity="start"
    android:background="@color/colorWhite"
    android:orientation="vertical"
    android:theme="@style/ThemeOverlay.AppCompat.Dark"
    >

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="#ededed"
        android:orientation="vertical"
        android:padding="10dp">

        <FrameLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal">

            <ImageView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:src="@drawable/img_info" />

            <ImageView
                android:id="@+id/btn_close"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="right|top"
                android:onClick="NaviItemSelected"
                android:src="@drawable/ic_close" />

        </FrameLayout>
    </LinearLayout>

</LinearLayout>
 


이제 자바 소스를 구현하는 걸 살펴보자.

public class MainActivity extends AppCompatActivity {
    Context context;
    DrawerLayout drawer_layout;
    ActionBarDrawerToggle toggle;
    //NavigationView navigationView;

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

        drawer_layout = findViewById(R.id.drawer_layout);
        toggle = new ActionBarDrawerToggle(
                this, drawer_layout, R.string.navigation_drawer_open, R.string.navigation_drawer_close);
        drawer_layout.addDrawerListener(toggle);
        toggle.syncState();

        // com.google.android.material.navigation.NavigationView 를 구현했을 때 처리 (구현 안해도 됨)
        //navigationView = findViewById(R.id.nav_view);
        //navigationView.setItemIconTintList(null);
        //navigationView.setNavigationItemSelectedListener(this);

        // content_main.xml 에서 지정한 버튼을 클릭하면 drawer 가 오픈되도록 함.
        FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
        fab.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
               drawer_layout.openDrawer(GravityCompat.START);
            }
        });

    }

    // drawer 오픈된 상태에서 실행할 때 동작 이벤트 처리
    public void NaviItemSelected(View view) {
        switch (view.getId()) {
            case R.id.btn_close:
                drawer_layout.closeDrawer(Gravity.LEFT, true);
                break;
            case R.id.btn_logout:
                backPressHandler.appShutdown();
                break;
        }
    }
}


구현을 위한 핵심사항만 살펴본 것이므로 실제 구현 예제는 다른 블로그를 찾아보면 도움될 것이다.

예전 예제들은 android.support.v4.widget.DrawerLayout 로 되어 있는데 androidx.drawerlayout.widget.DrawerLayout 로 변경되었다는 것만 알자.


참고하면 좋은 자료

https://coding-factory.tistory.com/207 네비게이션 Drawer 사용법

https://medium.com/quick-code/android-navigation-drawer-e80f7fc2594f



블로그 이미지

Link2Me

,
728x90

탭을 누를 때마다 아래 화면이 마치 페이지가 넘어가듯이 변하는 것을 ViewPager 를 이용해 구현이 가능하다.

예전에 사용하던 방식에서 계속 새로운 기능으로 개선이 되면서 ViewPager를 이용한 TabLayout 을 연동하는 것이 가장 좋은 방법이다.

material Design에서 제공하는 tabLayout 을 이용하는 방법으로 된 예제다.

https://www.youtube.com/watch?v=YWjyAfEacT8 유투브 동영상 강의 보고 테스트한 걸 적어두었다. (2020.3.27)

그런데 제대로 된 코드가 아니라서 새롭게 수정 구현하고 적어둔다.(Upated 2020.4.26)


앱 gradle 에 라이브러리 추가하기

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    implementation 'com.google.android.material:material:1.1.0'
}


TabLayout 사용하기

검색창에서 tab 을 입력하면 tabLayout 이 나온다. 마우스로 끌어다가 1번과 같이 놓는다.

material Design 에서 제공하는 tabLayout 이 추가된다.

하는 방법은 https://www.youtube.com/watch?v=YWjyAfEacT8 을 참조하면 된다.

ConstraintLayout 작성하는 방법에 대한 설명이라고 보면 된다.


https://stackoverflow.com/questions/31640563/how-do-i-change-a-tab-background-color-when-using-tablayout

에서 선택된 아이템 배경색을 지정하는 걸 참조해서 구현한다.


android:layout_marginTop="40dp" 으로 해줘야만 공간이 제대로 나오는 버그(?)가 있어서

LinearLayout 으로 변경해서 해결했다.


<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <com.google.android.material.tabs.TabLayout
        android:id="@+id/tab_layout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="40dp"
        app:tabTextColor="@color/colorDarkGray"
        app:tabSelectedTextColor="@color/colorWhite"
        app:layout_collapseMode="pin"
        app:tabGravity="fill"
        app:tabBackground="@drawable/tab_color_selector"
        app:layout_constraintBottom_toTopOf="@+id/vp_layout"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">

        <com.google.android.material.tabs.TabItem
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Monday" />

        <com.google.android.material.tabs.TabItem
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Tuesday" />

        <com.google.android.material.tabs.TabItem
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Wednesday" />
    </com.google.android.material.tabs.TabLayout>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/vp_layout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/tab_layout">

        <androidx.viewpager.widget.ViewPager
            android:id="@+id/viewPager"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

최근에는 ViewPager2 를 사용하라고 나오는 거 같은데 이건 나중에 테스트해볼 예정이다.


ViewPager 영역에 표시될 Fragment Layout 파일을 추가한다. 3개, 5개는 정하기 나름이다.

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="프레그먼트1"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
 

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="프레그먼트2"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="프레그먼트3"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>


Fragment1.java 파일

- Activity 에서 전달받은 데이터를 Fragment 에서 받아서 처리하는 코드를 추가했다.

import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;

public class Fragment1 extends Fragment {
    private View view;

    private String parameter;


    public static Fragment1 newInstance(String parameter){
        Fragment1 fragment1 = new Fragment1();
        Bundle args = new Bundle(); //Use bundle to pass data
        args.putString("parameter", parameter);
        instance.setArguments(args); //Finally set argument bundle to fragment
        return instance;
    }


    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (getArguments() != null) {
            parameter = getArguments().getString("parameter");
            Log.e("FragParameter", parameter);
        }
    }


    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        view = inflater.inflate(R.layout.fragment_1,container,false);
        return view;
    }


    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
    }
}

public class Fragment2 extends Fragment {
    private View view;

    public static Fragment2 newInstance(){
        Fragment2 fragment2 = new Fragment2();
        return fragment2;
    }

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        view = inflater.inflate(R.layout.fragment_2,container,false);
        return view;
    }
}
 

public class Fragment3 extends Fragment {
    private View view;

    public static Fragment3 newInstance(){
        Fragment3 fragment3 = new Fragment3();
        return fragment3;
    }

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        view = inflater.inflate(R.layout.fragment_3,container,false);
        return view;
    }
}
 



ViewPagerAdapter.java

import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentPagerAdapter;

import java.util.ArrayList;
import java.util.List;

class ViewPagerAdapter extends FragmentPagerAdapter {
    private final List<Fragment> mFragmentList = new ArrayList<>();
    private final List<String> mFragTitleList = new ArrayList<>();

    public ViewPagerAdapter(FragmentManager manager) {
        super(manager);
    }

    @Override
    public Fragment getItem(int position) {
        return mFragmentList.get(position);
    }

    @Override
    public int getCount() {
        return mFragmentList.size();
    }

    public void addFrag(Fragment fragment, String title) {
        mFragmentList.add(fragment);
        mFragTitleList.add(title);
    }

    @Override
    public CharSequence getPageTitle(int position) {
        return mFragTitleList.get(position);
    }
}


MainActivity.java

import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.FragmentPagerAdapter;
import androidx.viewpager.widget.ViewPager;

import android.os.Bundle;

import com.google.android.material.tabs.TabLayout;

public class MainActivity extends AppCompatActivity {
    private ViewPager viewPager;
    private ViewPagerAdapter adapter;
    private TabLayout tabLayout;


    String code;

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


        code = getIntent().getExtras().getString("code");
        Log.e("MainActivityCode",code);


        Fragment frag1 = new Fragment1().newInstance(code);

        viewPager = findViewById(R.id.viewPager);
        tabLayout = findViewById(R.id.tab_layout);
        adapter = new ViewPagerAdapter(getSupportFragmentManager());
        adapter.addFrag(frag1, "Web");
        adapter.addFrag(new Fragment2(), "사랑");
        adapter.addFrag(new Fragment3(), "우정");
        viewPager.setAdapter(adapter);
        tabLayout.setupWithViewPager(viewPager);
    }
}


여기까지 하면 잘 동작할 것이다.


참고하면 좋을 자료

출처 : https://guides.codepath.com/android/viewpager-with-fragmentpageradapter

public class FirstFragment extends Fragment {
    // Store instance variables
    private String title;
    private int page;

    // newInstance constructor for creating fragment with arguments
    public static FirstFragment newInstance(int page, String title) {
        FirstFragment fragmentFirst = new FirstFragment();
        Bundle args = new Bundle();
        args.putInt("someInt", page);
        args.putString("someTitle", title);
        fragmentFirst.setArguments(args);
        return fragmentFirst;
    }

    // Store instance variables based on arguments passed
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        page = getArguments().getInt("someInt", 0);
        title = getArguments().getString("someTitle");
    }

    // Inflate the view for the fragment based on layout XML
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_first, container, false);
        TextView tvLabel = (TextView) view.findViewById(R.id.tvLabel);
        tvLabel.setText(page + " -- " + title);
        return view;
    }
}
 


블로그 이미지

Link2Me

,
728x90
안드로이드 10 에서는 TelephonyManager에서 개인을 특정할 수 있는 정보를 가져올수 없도록 변경되었다.

TelephonyManager().getDeviceId()
TelephonyManager().getImei()
TelephonyManager().getMeid()

그리고 하드웨어의 시리얼넘버도 사용이 불가능해졌다.
Builde.SERIAL


In android 10, couldn't get device id using permission "READ_PHONE_STATE".


getDeviceId() has been deprecated since API level 26.
You can use an instance id from firebase e.g FirebaseInstanceId.getInstance().getId();.
String deviceId = android.provider.Settings.Secure.getString(
                context.getContentResolver(), android.provider.Settings.Secure.ANDROID_ID);


삼성갤럭시 S10(Android 10) 에서 테스트한 결과 코드를 적어둔다.

기존 안드로이드 폰은 IMEI 값이 15자리 숫자로만 되어 있었는데

아래 코드에서 제공하는 deviceID값은 IMEI 값이 아닌 다른 값을 제공하는 거 같다.

개발자모드에서 수집한 deviceID 와 release 모드로 앱을 만든 것과 값이 다르더라.

cc64a5b80853bc8d

36f7660af1dc22f2


public static String getDeviceId(Context context) {

    // 단말기의 ID 정보를 얻기 위해서는 READ_PHONE_STATE 권한이 필요
    String deviceId;

    if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        deviceId = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID);
    } else {
        final TelephonyManager mTelephony = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
        if (ActivityCompat.checkSelfPermission(context, Manifest.permission.READ_PHONE_STATE) != PackageManager.PERMISSION_GRANTED) {
            //권한 없을 경우
            if (ActivityCompat.shouldShowRequestPermissionRationale((Activity) context, Manifest.permission.READ_PHONE_STATE)) {
                // 사용자에게 해당 권한이 필요한 이유에 대해 설명
                Toast.makeText(context, "앱 실행을 위해서는 전화 관리 권한을 설정해야 합니다.", Toast.LENGTH_SHORT).show();
            }
            // 최초로 권한을 요청하는 경우
            ActivityCompat.requestPermissions((Activity) context, new String[]{Manifest.permission.READ_PHONE_STATE} , 2);
            return null;
        } else {
            Log.v("TAG","Permission is granted");
            if (mTelephony.getDeviceId() != null) {
                deviceId = mTelephony.getDeviceId();
            } else {
                deviceId = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID);
            }
        }
    }
    return deviceId;
}

@Override
public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) {
    switch (requestCode) {
        case 2: {
            if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                Toast.makeText(getApplicationContext(), "Permission granted", Toast.LENGTH_SHORT).show();
            } else {
                Toast.makeText(getApplicationContext(), "Permission denied", Toast.LENGTH_SHORT).show();
            }
            return;
        }
    }
}
 


만약 deviceId = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID); 가 제대로 동작하지 않는다면, 다른 대책을 세워야 할 거 같다.


if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        //deviceId = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID);
        deviceId = getUniqueID(context);
}


public synchronized static String getUniqueID(Context context) {
    // 자바에서는 기본적으로 UUID를 생성하는 클래스를 지원한다.
    // UUID를 생성하면 간단하게 유니크한 ID를 얻을 수 있다.
    if (uniqueID == null) {
        SharedPreferences sharedPrefs = context.getSharedPreferences(PREF_UNIQUE_ID, Context.MODE_PRIVATE);
        uniqueID = sharedPrefs.getString(PREF_UNIQUE_ID, null);
        if (uniqueID == null) {
            uniqueID = UUID.randomUUID().toString().replace("-", "");
            Log.e("UUID : ",uniqueID);
            SharedPreferences.Editor editor = sharedPrefs.edit();
            editor.putString(PREF_UNIQUE_ID, uniqueID);
            editor.commit();
        }
    }
    // 앱을 삭제하면 정보 삭제되므로 앱 삭제시에는 관리자에게 초기화 요청 필요
    return uniqueID;
}



Eclipse 테스트 일시 : 2020.3.19일

오래전에 Eclipse 개발툴로 개발한 걸 테스트 해봤다.

디바이스를 구분하는 고유 번호로  ANDROID_ID 로 대체하면 해결되더라.

릴리즈 버전과 디버깅 버전 APK 의 Android ID 가 다르지만 사용자에게는 문제는 없다.

//TelephonyManager tm = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE);
String uID = Settings.Secure.getString(getApplicationContext().getContentResolver(), Settings.Secure.ANDROID_ID);


블로그 이미지

Link2Me

,
728x90

안드로이드폰에서 폴더에 있는 text 파일을 읽어서 미리보기하는 기능이 필요하여 구현해봤다.


테스트 환경 : LG G5 테스트 성공, Samsung S10 실패


XML 파일

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <Button
        android:id="@+id/btn_fileread"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="20dp"
        android:layout_gravity="center"
        android:text="File Read"
        android:textAllCaps="false"/>
</LinearLayout>

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/linear_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:background="#ffffff">

    <LinearLayout
        android:orientation="horizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <TextView
            android:id="@+id/popupTitle"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="3"
            android:gravity="center"
            android:text="읽은 파일 미리보기"
            android:textSize="18sp"
            android:textStyle="bold"
            android:background="@color/colorPrimary"/>

        <TextView
            android:id="@+id/popupSaveBtn"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:gravity="center"
            android:text="적용"
            android:textSize="18sp"
            android:textStyle="bold"
            android:background="#B5FFD5"/>

        <TextView
            android:id="@+id/popupCancelBtn"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:gravity="center"
            android:text="취소"
            android:textSize="18sp"
            android:textStyle="bold"
            android:background="#FFFF00"/>

    </LinearLayout>

    <ScrollView
        android:id="@+id/popupScrollView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#000000">

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

            <TextView
                android:id="@+id/popupText"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginLeft="10dp"
                android:ellipsize="start"
                android:gravity="left"
                android:textColor="#FFFFFF"
                android:textStyle="bold" />
        </LinearLayout>
    </ScrollView>

</LinearLayout>


갤럭시 S10에서 안되는 이유는 사용된 코드가 오래된 코드라서 그렇더라.

인터넷에서 찾아서 테스트한 코드가 매우 오래된 코드라서 보안이 강화된 폰에서는 읽어내질 못하더라.

주황색으로 표시된 코드를 다른 코드로 찾아서 대체하면 해결될 것이다.

최신폰에서 동작되도록 하는 것은 개발자 각자의 몫으로....

미리 보기를 위한 팝업창 코드에는 전혀 문제가 없다는 것도 확인했다.

에러 때문에 좀 삽질을 하기는 했지만.


public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    private static final String TAG = "MainActivity";
    Context mContext;

    Button popup_alert;
    String select_filePath;
    ScrollView pScrollView;
    View popupView;

    private static final int READ_REQUEST_CODE = 42;

    protected View mPopupView; // 팝업 뷰

    private void initView() {
       // 1. 특정 폴더에 있는 파일 읽어서 미리보기 화면에 보여주기
        popup_alert = findViewById(R.id.btn_fileread);
        popup_alert.setOnClickListener(this);
    }

    @Override
    public void onClick(View view) {
        switch (view.getId()) {
            case R.id.btn_fileread:
                popupView = view;
                showFileChooser();
                break;
        }
    }

    private void showFileChooser() {
        Intent intent;
        if (android.os.Build.MANUFACTURER.equalsIgnoreCase("samsung")) {
            intent = new Intent("com.sec.android.app.myfiles.PICK_DATA");
            intent.putExtra("CONTENT_TYPE", "*/*");
            intent.addCategory(Intent.CATEGORY_DEFAULT);
        } else {

            String[] mimeTypes =
                    {"application/msword", "application/vnd.openxmlformats-officedocument.wordprocessingml.document", // .doc & .docx
                            "application/vnd.ms-powerpoint", "application/vnd.openxmlformats-officedocument.presentationml.presentation", // .ppt & .pptx
                            "application/vnd.ms-excel", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", // .xls & .xlsx
                            "text/plain",
                            "application/pdf",
                            "application/zip", "application/vnd.android.package-archive"};

            intent = new Intent(Intent.ACTION_GET_CONTENT); // or ACTION_OPEN_DOCUMENT
            intent.setType("*/*");
            intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes);
            intent.addCategory(Intent.CATEGORY_OPENABLE);
            intent.putExtra(Intent.EXTRA_LOCAL_ONLY, true);
        }

        try {
            startActivityForResult(Intent.createChooser(intent, "Select a File"), READ_REQUEST_CODE);
        } catch (android.content.ActivityNotFoundException ex) {
            Toast.makeText(this, "Please install a File Manager.",Toast.LENGTH_SHORT).show();
        }
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        switch (requestCode) {
            case READ_REQUEST_CODE:
                if (resultCode == RESULT_OK) {
                    // Get the Uri of the selected file
                    Uri uri = data.getData();
                    select_filePath = getRealPathFromURI(uri); // 이 함수를 다른 걸로 대체하는게 공부
                    Log.e(TAG, "file_path = " + select_filePath);
                    if(select_filePath != null){
                        mPopupWindowShow(readFile(select_filePath));
                    }
                }
                break;
        }
        super.onActivityResult(requestCode, resultCode, data);
    }

    private String getRealPathFromURI(Uri uri) {
        String filePath = "";
        filePath = uri.getPath();
        //경로에 /storage가 들어가면 real file path로 판단
        if (filePath.startsWith("/storage"))
            return filePath;

        String wholeID = DocumentsContract.getDocumentId(uri);

        //wholeID는 파일명이 abc.zip이라면 /document/B5D7-1CE9:abc.zip와 같다.
        // Split at colon, use second item in the array
        String id = wholeID.split(":")[1];

        Log.e(TAG, "id = " + id);

        String[] column = { MediaStore.Files.FileColumns.DATA };

        //파일의 이름을 통해 where 조건식을 만든다.
        String sel = MediaStore.Files.FileColumns.DATA + " LIKE '%" + id + "%'";

        //External storage에 있는 파일의 DB를 접근하는 방법이다.
        Cursor cursor = getContentResolver().query(MediaStore.Files.getContentUri("external"),
                column, sel, null, null);

        int columnIndex = cursor.getColumnIndex(column[0]);

        if (cursor.moveToFirst()) {
            filePath = cursor.getString(columnIndex);
        }
        cursor.close();
        return filePath;
    }

    private String readFile(String filepath) {
        File fileEvents = new File(filepath);
        StringBuilder text = new StringBuilder();
        try {
            BufferedReader br = new BufferedReader(new FileReader(fileEvents));
            String line;
            while ((line = br.readLine()) != null) {
                text.append(line);
                text.append('\n');
            }
            br.close();
        } catch (IOException e) { }
        String result = text.toString();
        return result;
    }

    public void mPopupWindowShow(String url) {
        Display display = ((WindowManager)getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
        Point point = new Point();
        display.getSize(point);
        int height = (int) (point.y * 0.8);
        int width = (int) (point.x * 0.9); // Display 사이즈의 90%

        // inflate the layout of the popup window
        LayoutInflater inflater = (LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE);
        LinearLayout layout = (LinearLayout) inflater.inflate(R.layout.popup_window,null);
        mPopupView = layout.findViewById(R.id.linear_layout);

        TextView popupTitle = layout.findViewById(R.id.popupTitle);
        popupTitle.setText("내용 미리보기");
        TextView popupSave = layout.findViewById(R.id.popupSaveBtn);

        TextView popupCancel = layout.findViewById(R.id.popupCancelBtn);

        pScrollView = layout.findViewById(R.id.popupScrollView);
        TextView popupContent = layout.findViewById(R.id.popupText);
        popupContent.setText(url);

        pScrollView.setVerticalScrollBarEnabled(true); // 수직방향 스크롤바 사용 가능하도록 설정
        popupContent.setMovementMethod(new ScrollingMovementMethod());
        popupContent.setTextSize(12);

        // create the popup window
        boolean focusable = true; // lets taps outside the popup also dismiss it
        final PopupWindow popupWindow = new PopupWindow(mPopupView, width, height, focusable);

        popupWindow.showAtLocation(popupView, Gravity.BOTTOM, 0, 0); // Parent View 로부터의 위치

        popupSave.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(mContext, "적용을 시작합니다.", Toast.LENGTH_SHORT).show();
            }
        });

        popupCancel.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (popupWindow != null && popupWindow.isShowing()){
                    popupWindow.dismiss();
                }
            }
        });
    }

}




블로그 이미지

Link2Me

,
728x90

안드로이드 바인딩 서비스 개요

startService() 메소드 대신 bindService() 메소드를 통해 시작되는 서비스를 서비스 바인딩이라 한다.

바인딩 서비스는 Activity가 클라이언트 역할을 하고, 서비스가 서버 역할을 한다.

startService() 메소드 대신 bindService() 메소드를 통해 시작되는 서비스를 서비스 바인딩이라 한다.

이 서비스는 Activity가 클라이언트 역할을 하고, 서비스가 서버 역할을 한다.



출처: https://link2me.tistory.com/1343 [소소한 일상 및 업무TIP 다루기]

BindService는 startService()를 통해 시작되는 UnBound Service와는 다르게
Activity / Fragment와 서비스간에 데이터를 주고 받을 수 있으며, 프로세스간의 통신에도 사용된다.

백그라운드에서 무한히 실행되지 않도록 Activity 종료시 자동으로 서비스를 종료시킬 수 있도록 구현해야 한다.

서비스 바인딩은 연결된 Activity가 사라지면 서비스도 소멸된다.
하나의 서비스에 다수의 Activity 연결이 가능하다.
서비스에 연결된 Activity(컴포넌트)가 하나도 남아있지 않으면 서비스는 종료된다.

서비스 바인딩 및 구현 동작

연결을 유지하고 데이터를 전송 받기 위한 ServiceConnection() 객체와 IBinder 인터페이스 객체가 필요하다.

ServiceConnection() 는 Service를 호출하는 Activity 에 만들어야 하고, IBinder 는 Service 에서 생성한 후 리턴해야 한다.


startService()를 호출하여 서비스를 시작하고 이를 통해 서비스가 무한히 실행되도록 할 수 있으며, bindService()를 호출하면 클라이언트(Activity)가 해당 서비스에 바인딩되도록 할 수 있다는 의미다.


서비스가 시작되고 바인딩되도록 허용한 다음, 서비스가 실제로 시작되면 시스템은 Activity가 모두 바인딩을 해제해도 서비스를 소멸시키지 않는다. 그 대신 서비스를 직접 확실히 중단해야 한다. 그러려면 stopSelf() 또는 stopService()를 호출하면 된다.


보통은 onBind() 또는 onStartCommand() 중 한 가지만 구현하지만, 둘 모두 구현해야 할 때도 있다.
클라이언트는 bindService()를 호출하여 서비스에 바인딩된다.
이때 반드시 서비스와의 연결을 모니터링하는 ServiceConnection의 구현을 제공해야 한다.
Android 시스템이 클라이언트와 서비스 사이에 연결을 설정하면 ServiceConnection에서 onServiceConnected()을 호출한다.
onServiceConnected() 메서드에는 IBinder 인수가 포함되고 클라이언트는 이를 사용하여 바인딩된 서비스와 통신한다.
바인딩된 서비스를 구현할 때 가장 중요한 부분은 onBind() 콜백 메서드가 반환하는 인터페이스를 정의하는 것이다.

바인딩 서비스 예제

안드로이드 서비스 예제를 통해 개념을 익히고 활용할 수 있도록 예제를 테스트하고 적어둔다.

https://link2me.tistory.com/1343 에 기본 서비스 예제가 있으며, 비교하여 달라진 점을 확인해 볼 수 있다.

bindService를 추가하면 서비스 종료시에 반드시 unbindService(mConnection) 처리를 해줘야만 제대로 종료가 됨을 확인할 수 있다.


layout.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="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/btn_servicestart"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:gravity="center_vertical"
        android:layout_gravity="center"
        android:text="서비스 시작"
        android:textSize="14sp"
        android:textAllCaps="false"
        android:layout_marginTop="30dp"/>

    <Button
        android:id="@+id/btn_servicestop"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="서비스 종료"
        android:layout_gravity="center"
        android:gravity="center"
        android:textSize="14sp"
        android:textAllCaps="false"
        android:layout_marginTop="20dp"/>

    <Button
        android:id="@+id/btn_bindservice"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="바인드 값"
        android:layout_gravity="center"
        android:gravity="center"
        android:textSize="14sp"
        android:textAllCaps="false"
        android:layout_marginTop="20dp"/>

</LinearLayout>



MyService.java

import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.Binder;
import android.os.IBinder;
import android.util.Log;

public class MyService extends Service {
    private static final String TAG = MyService.class.getSimpleName();
    private Context context;
    private int mCount = 0;
    private Thread mThread;
    public static boolean SERVICE_CONNECTED = false;

    public MyService() {
    }

    private IBinder mBinder = new MyBinder();
    public class MyBinder extends Binder {
        public MyService getService(){
            Log.e(TAG, "MyService MyBinder return.");
            return MyService.this;  // 서비스 객체를 리턴
        }
    }

    @Override
    public IBinder onBind(Intent intent) {
        Log.e(TAG, "MyService IBinder onBind");
        // Service 객체와 (화면단 Activity 사이에서) 데이터를 주고받을 때 사용하는 메서드
        // Activity에서 bindService() 를 실행하면 호출됨
        // 데이터를 전달할 필요가 없으면 return null;
        return mBinder; // 리턴한 mBinder 객체는 서비스와 클라이언트 사이의 인터페이스를 정의한다.
    }

    public int getCount(){
        return mCount;
    }

    @Override
    public void onCreate() {
        Log.e(TAG, "MyService Started");
        this.context = this;
        MyService.SERVICE_CONNECTED = true;
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.e(TAG, "MyService onStartCommand startID === "+startId); // 계속 증가되는 값
        if(mThread == null){
            mThread = new Thread("My Thread"){
                @Override
                public void run() {
                    while (!Thread.currentThread().isInterrupted()){
                        try {
                            mCount++;
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                            this.interrupt();
                        }
                        Log.e("My Thread", "서비스 동작 중 " + mCount);
                    }
                }
            };
            mThread.start();
        }
        return START_STICKY;
    }

    @Override
    public void onDestroy() {
        Log.e(TAG,"MyService onDestroy");
        super.onDestroy();
        if(mThread != null && mThread.isAlive() ){
            mThread.interrupt();
            mThread = null;
            mCount = 0;
        }
        SERVICE_CONNECTED = false;
    }
}


MainActivity.java

import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;

import androidx.appcompat.app.AppCompatActivity;

import java.util.Set;

public class MainActivity extends AppCompatActivity {
    private static final String TAG = MainActivity.class.getSimpleName();
    Context mContext;
    private Button btnServiceStart;
    private Button btnServiceStop;
    private Button btnBindService;

    private MyService myService;
    private boolean mBound = false;

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

        btnServiceStart = findViewById(R.id.btn_servicestart);
        btnServiceStop = findViewById(R.id.btn_servicestop);
        btnBindService = findViewById(R.id.btn_bindservice);

        btnServiceStart.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(mContext, "서비스를 시작합니다.", Toast.LENGTH_SHORT).show();
                Intent intent = new Intent(mContext,MyService.class);
                startService(intent);
            }
        });

        btnServiceStop.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(mContext, "서비스를 종료합니다.", Toast.LENGTH_SHORT).show();
                Log.e(TAG,"MainActivity ServiceStop Button Clicked.");
                if(mBound){
                    unbindService(mConnection);
                    mBound = false;
                }
                stopService(new Intent(mContext,MyService.class));
            }
        });

        btnBindService.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if(mBound){
                    Toast.makeText(mContext, "카운팅 : " + myService.getCount(), Toast.LENGTH_SHORT).show();
                }
            }
        });
    }

    @Override
    protected void onDestroy() {
        Log.e(TAG,"MainActivity onDestroy");
        if(mBound){
            unbindService(mConnection);
            mBound = false;
        }
        stopService(new Intent(this,MyService.class));
        super.onDestroy();
    }

    @Override
    protected void onStart() {
        super.onStart();
        Intent intent = new Intent(this,MyService.class);
        bindService(intent,mConnection,BIND_AUTO_CREATE); //
startService 버튼 클릭시 시작
//        startService(MyService.class, mConnection,null); // Activity 시작시
startService 자동 실행
    }

    private ServiceConnection mConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            MyService.MyBinder binder = (MyService.MyBinder) service;
            myService = binder.getService();
            mBound = true;
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            // 예기치 않은 종료(안드로이드 OS에 의한 종료)

            mBound = false;
        }
    };

    private void startService(Class<?> service, ServiceConnection serviceConnection, Bundle extras) {
        // onStart 메소드에서 서비스 자동 실행 처리할 목적
        if (!MyService.SERVICE_CONNECTED) {
            Intent startService = new Intent(this, service);
            if (extras != null && !extras.isEmpty()) {
                Set<String> keys = extras.keySet();
                for (String key : keys) {
                    String extra = extras.getString(key);
                    startService.putExtra(key, extra);
                }
            }
            startService(startService);
        }
        Intent bindingIntent = new Intent(this, service);
        bindService(bindingIntent, serviceConnection, Context.BIND_AUTO_CREATE);
    }
}


Log 를 통해서 동작 순서에 대한 이해를 할 수 있다.

Activity가 실행되면서 자동으로 서비스를 실행하도록 한 경우의 Log 메시지다.


블로그 이미지

Link2Me

,
728x90

안드로이드 폰에서 SSH 어플을 사용하고자 한다면, 검색해보면 가장 추천하는 앱은 JuiceSSH 이고, open source project로 진행되는 ConnectBot 이 있다는 걸 알았다.  좀 더 실력이 키워야 분석 및 활용이 가능할 것 같다.


Java 언어로 개발된 SSH2 라이브러리는 JSCH(Java Secure Channel) 를 확인할 수 있다.

구글링하면 예제 많이 나오는데 제대로 동작되는 걸 해보려면 쉽지 않더라.

http://www.jcraft.com/jsch/ 에 가면 jsch-0.1.55.jar 파일을 다운로드할 수 있다.


https://github.com/jonghough/AndroidSSH 에서 소스 코드를 다운로드 받아서 어플을 실행해 볼 수 있다.


테스트 결과 LG G5 안드로이드 6.0 운영체제에서는 잘 동작을 했다.

그런데 삼성 갤럭시 S7 안드로이드 8.0, 삼성 갤럭시 S10 안드로이드 9.0 운영체제 폰에서는 동작이 제대로 안되더라.

코드가 오래전에 올려진 것이라서 위험권한을 추가하고 동작시켜도 동작이 안된다.

쓰레드 처리에 대한 학습을 좀 하고 나서 몇가지 사항을 수정해서 동작되도록 했다.


앱 build.gradle

android {
    compileSdkVersion 28


   // 중간 생략


dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])

    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    implementation 'gun0912.ted:tedpermission:2.0.0'
    implementation 'androidx.multidex:multidex:2.0.0' // minSdkVersion 이 21 이하인 경우 사용
    implementation 'com.jcraft:jsch:0.1.55'
    implementation 'org.bouncycastle:bcprov-jdk16:1.46'
}


MainActivity.java 수정사항

- 위험권한 설정 처리코드 추가 구현 필요


ShellController.java 수정사항

    public void writeToOutput(final String command) {
        if (mDataOutputStream != null) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        mDataOutputStream.writeBytes(command + "\r\n");
                        mDataOutputStream.flush();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }).start();

        }
    }


   public void openShell(final Session session, Handler handler, EditText editText) throws JSchException, IOException {
        Log.e(TAG, "openShell start!");
        if (session == null) throw new NullPointerException("Session cannot be null!");
        if (!session.isConnected()) throw new IllegalStateException("Session must be connected.");
        final Handler myHandler = handler;
        final EditText myEditText = editText;
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    mChannel = session.openChannel("shell");
                    mChannel.connect();
                    try {
                        mBufferedReader = new BufferedReader(new InputStreamReader(mChannel.getInputStream()));
                        mDataOutputStream = new DataOutputStream(mChannel.getOutputStream());
                    } catch (IOException e) {
                        e.printStackTrace();
                    }

                } catch (JSchException e) {
                    e.printStackTrace();
                }

                try {
                    String line;
                    while (true) {
                        while ((line = mBufferedReader.readLine()) != null) {
                            final String result = line;
                            if (mSshText == null) mSshText = result;
                            Log.e(TAG, "result : " + result);
                            myHandler.post(new Runnable() {
                                public void run() {
                                    synchronized (myEditText) {
                                        ((SshEditText)myEditText).setPrompt(result); //set the prompt to be the current line, so eventually it will be the last line.
                                        myEditText.setText(myEditText.getText().toString() + "\r\n" + result + "\r\n"+fetchPrompt(result));
                                        Log.e(TAG, "LINE : " + result);
                                    }
                                }
                            });
                        }

                    }
                } catch (Exception e) {
                    Log.e(TAG, " Exception " + e.getMessage() + "." + e.getCause() + "," + e.getClass().toString());
                }
            }
        }).start();
    }
 


내가 테스트한 GitHub 소스는 칼라 코드 지원은 안된다.

SFTP 기능 구현은 테스트 하지 않았다.



참고하면 도움될 게시글
싱글톤(Singleton) 개념 이해 : https://link2me.tistory.com/1528
Java Thread 이해 및 Thread Life Cycle : https://link2me.tistory.com/1730
Java Thread 상태 제어 : https://link2me.tistory.com/1731
Java Thread 동기화 : https://link2me.tistory.com/1732


블로그 이미지

Link2Me

,
728x90

안드로이드 AlarmManager 를 이용한 알람 예제를 테스트 하고 적어둔다.

서비스 개념을 이해하는데에는 도움이 되지만 완벽한 앱 알람 기능을 해결해주진 않는거 같다.

Job Scheduler 로 기능을 구현해 봐야 하나 싶은건 아닌가 싶기도 한데 아직 안해봐서 ....


출처는 https://sh-itstory.tistory.com/64 에서 제공한 https://github.com/aiex1234/PleaseWakeMeUp 에서 파일을 다운로드 받았다. 코드 덕분에 좀 더 편하게 활용 테스트가 가능했다.


삼성 갤럭시 S10(Android 9) 와 LG G5(Android 6.01) 두가지 폰에서 테스트했고 여타 자료를 참조하여 코드를 보강하였다.

알람을 설정한 시간이 되면 음악이 자동 재생되고, Stop 버튼을 누르면 재생이 멈추는 기능은 완벽하게 동작한다.

서비스에 대한 개념 이해는 https://link2me.tistory.com/1343 참조하면 된다.


Notification 메시지 보내는 것은 https://link2me.tistory.com/1514 에 올려진 파일을 받아서 androidX 로 변환해서 사용하면 100% 동작한다.

알람메시지를 받으면 띄워줄 Activity 가 MainActivity 이므로 MainActivity.class 로 수정하고, MainActivity.java 파일내에

        NotificationHelper notificationHelper = new NotificationHelper(mContext);
        notificationHelper.cancelNotification(mContext,0);
를 추가해주면 어플이 실행되고 있지 않아도 지정된 시간이 되면 Broadcast 메시지가 동작하여 음악을 재생하고 Notification 을 보내준다.

누르면 MainActivity 화면이 나오고, 종료 버튼을 누르면 재생되던 음악이 종료된다.


날짜를 지정하고, 시간을 설정하는 것까지 추가하면 알람 앱 기본 기능으론 충분하다고 본다.


앱 build.gradle

apply plugin: 'com.android.application'

android {
    compileSdkVersion 28

    defaultConfig {
        applicationId "com.link2me.android.wakemeup"
        minSdkVersion 19
        targetSdkVersion 28
        versionCode 1
        versionName "1.0"
    }

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

}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'androidx.appcompat:appcompat:1.0.2'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
}


기본적으로 디바이스를 종료하면 모든 알람은 취소된다.

이러한 상황이 벌어지는 것을 막기 위해, 유저가 디바이스를 재부팅했을 때 앱이 자동적으로 알람을 재구동 하도록 설계할 수 있다.

앱 매니페스트에 RECEIVE_BOOT_COMPLETED 권한을 설정한다. 이 액션은 시스템 부팅 완료 후 브로드캐스트인 ACTION_BOOT_COMPLETED 을 받을 수 있도록 해준다.

이와 관련된 코드 구현은 하지 않았다.


AndroidManifest.xml

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

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.WAKE_LOCK" />
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />

    <application
        android:allowBackup="true"
        android:icon="@drawable/icon_watch"
        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>
        <receiver android:name=".AlarmReceiver" />

        <service
            android:name=".RingtoneService"
            android:enabled="true"></service>

    </application>

</manifest>


activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity"
    android:orientation="vertical"
    android:gravity="top">

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

        <TimePicker
            android:id="@+id/time_picker"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />

    </LinearLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:gravity="center">

        <Button
            android:layout_width="60dp"
            android:layout_height="50dp"
            android:id="@+id/btn_alarmStart"
            android:text="시작"/>

        <Button
            android:layout_width="60dp"
            android:layout_height="50dp"
            android:id="@+id/btn_alarmFinish"
            android:text="종료" />

    </LinearLayout>

    <TextView
        android:id="@+id/tv_alarmON"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:gravity="center"
        android:layout_marginTop="30dp"
        android:text="알람 예정 시간"
        android:textSize="14dp"
        android:textColor="@color/colorAccent"/>

</LinearLayout>


MainActivity.java

package com.link2me.android.wakemeup;

import android.app.Activity;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.widget.TimePicker;
import android.widget.Toast;

import androidx.annotation.RequiresApi;
import androidx.appcompat.app.AppCompatActivity;

import com.link2me.android.common.BackPressHandler;

import java.util.Calendar;

public class MainActivity extends AppCompatActivity {
    private static final String TAG = MainActivity.class.getSimpleName();
    Context mContext;
    TextView textView;
    TimePicker alarm_timepicker;
    AlarmManager alarm_manager;
    String am_pm;
    int getHourTimePicker = 0;
    int getMinuteTimePicker = 0;

    Intent alarmIntent;
    PendingIntent pendingIntent;
    private static final int REQUEST_CODE = 1111;
    SharedPreferences pref;

    private BackPressHandler backPressHandler;

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

        textView = findViewById(R.id.tv_alarmON);
        textView.setText("");

        // 알람매니저 설정
        alarm_manager = (AlarmManager) mContext.getSystemService(ALARM_SERVICE);
        // 알람매니저가 유용한 이유는, Alarm이 한번 등록되면 어플리케이션의 생명주기와 관계없이
        // 어플리케이션이 종료되어있는 경우에도 지정해놓은 operation에 대해 어김없이 실행할 수 있다는 것

        // 타임피커 설정
        alarm_timepicker = findViewById(R.id.time_picker);
        alarm_timepicker.setIs24HourView(true);

        // 알람 Receiver 인텐트 생성
        alarmIntent = new Intent(mContext, AlarmReceiver.class);

        // Button Alarm ON
        Button alarm_on = findViewById(R.id.btn_alarmStart);
        alarm_on.setOnClickListener(new View.OnClickListener() {
            @RequiresApi(api = Build.VERSION_CODES.M)
            @Override
            public void onClick(View v) {
                setAlarm(mContext);
            }
        });

        Button alarm_off = findViewById(R.id.btn_alarmFinish);
        alarm_off.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                releaseAlarm(mContext);
            }
        });
    }

    private void setAlarm(Context context){
        // Calendar 객체 생성
        final Calendar calendar = Calendar.getInstance();

        // calendar에 시간 셋팅
        if (Build.VERSION.SDK_INT < 23) {
            // 시간 가져옴
            getHourTimePicker = alarm_timepicker.getCurrentHour();
            getMinuteTimePicker = alarm_timepicker.getCurrentMinute();
        } else {
            // 시간 가져옴
            getHourTimePicker = alarm_timepicker.getHour();
            getMinuteTimePicker = alarm_timepicker.getMinute();
        }

        // 현재 지정된 시간으로 알람 시간 설정
        calendar.setTimeInMillis(System.currentTimeMillis());
        calendar.set(Calendar.HOUR_OF_DAY, getHourTimePicker);
        calendar.set(Calendar.MINUTE, getMinuteTimePicker);
        calendar.set(Calendar.SECOND, 0);

        pref = getSharedPreferences("pref", Activity.MODE_PRIVATE);
        SharedPreferences.Editor editor = pref.edit();
        editor.putInt("set_hour", getHourTimePicker);
        editor.putInt("set_min", getMinuteTimePicker);
        editor.putString("state", "ALARM_ON");
        editor.commit();

        // reveiver에 string 값 넘겨주기
        alarmIntent.putExtra("state","ALARM_ON");

        // receiver를 동작하게 하기 위해 PendingIntent의 인스턴스를 생성할 때, getBroadcast 라는 메소드를 사용
        // requestCode는 나중에 Alarm을 해제 할때 어떤 Alarm을 해제할지를 식별하는 코드
        pendingIntent = PendingIntent.getBroadcast(mContext,REQUEST_CODE,alarmIntent,PendingIntent.FLAG_UPDATE_CURRENT);

        long currentTime = System.currentTimeMillis(); // 현재 시간
        //long triggerTime = SystemClock.elapsedRealtime() + 1000*60;
        long triggerTime = calendar.getTimeInMillis(); // 알람을 울릴 시간
        long interval = 1000 * 60 * 60  * 24; // 하루의 시간

        while(currentTime > triggerTime){ // 현재 시간보다 작다면
            triggerTime += interval; // 다음날 울리도록 처리
        }
        Log.e(TAG, "set Alarm : " + getHourTimePicker + " 시 " + getMinuteTimePicker + "분");

        // 알림 세팅 : AlarmManager 인스턴스에서 set 메소드를 실행시키는 것은 단발성 Alarm을 생성하는 것
        // RTC_WAKEUP : UTC 표준시간을 기준으로 하는 명시적인 시간에 intent를 발생, 장치를 깨움
        if (Build.VERSION.SDK_INT < 23) {
            if (Build.VERSION.SDK_INT >= 19) {
                alarm_manager.setExact(AlarmManager.RTC_WAKEUP, triggerTime, pendingIntent);
            } else {
                // 알람셋팅
                alarm_manager.set(AlarmManager.RTC_WAKEUP, triggerTime, pendingIntent);
            }
        } else {  // 23 이상
            alarm_manager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, triggerTime, pendingIntent);
            //alarm_manager.set(AlarmManager.RTC_WAKEUP, triggerTime,pendingIntent);
            //알람 매니저를 통한 반복알람 설정
            //alarm_manager.setRepeating(AlarmManager.RTC, triggerTime, interval, pendingIntent);
            // interval : 다음 알람이 울리기까지의 시간
        }

        // Unable to find keycodes for AM and PM.
        if(getHourTimePicker > 12){
            am_pm = "오후";
            getHourTimePicker = getHourTimePicker - 12;
        } else {
            am_pm ="오전";
        }
        textView.setText("알람 예정 시간 : "+ am_pm +" "+ getHourTimePicker + "시 " + getMinuteTimePicker + "분");
    }

    public void releaseAlarm(Context context)  {
        Log.e(TAG, "unregisterAlarm");

        pref = getSharedPreferences("pref", Activity.MODE_PRIVATE);
        SharedPreferences.Editor editor = pref.edit();
        editor.putString("state", "ALARM_OFF");
        editor.commit();

        // 알람매니저 취소
        alarm_manager.cancel(pendingIntent);
        alarmIntent.putExtra("state","ALARM_OFF");

        // 알람 취소
        sendBroadcast(alarmIntent);

        Toast.makeText(MainActivity.this,"Alarm 종료",Toast.LENGTH_SHORT).show();
        textView.setText("");
    }

    @Override
    public void onBackPressed() {
        backPressHandler.onBackPressed();
    }

}


AlarmReceiver.java

package com.link2me.android.wakemeup;

import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.media.MediaPlayer;
import android.os.PowerManager;
import android.util.Log;

public class AlarmReceiver extends BroadcastReceiver{
    private static final String TAG = AlarmReceiver.class.getSimpleName();
    Context mContext;

    PowerManager powerManager;
    private static PowerManager.WakeLock wakeLock;
    SharedPreferences pref;
    String get_state;
    private MediaPlayer mediaPlayer;

    @Override
    public void onReceive(Context context, Intent intent) {
        mContext = context;
        pref = context.getSharedPreferences("pref", Activity.MODE_PRIVATE);
        get_state = pref.getString("state","");
        Log.e(TAG, "Alarm state : " + get_state);

        AlarmReceiverChk(context, intent);
    }

    private void AlarmReceiverChk(final Context context, final Intent intent){
        Log.d(TAG, "Alarm Receiver started!");
        switch (get_state){
            case "ALARM_ON":
                acquireCPUWakeLock(context, intent);
                // RingtoneService 서비스 intent 생성
                Intent serviceIntent = new Intent(mContext, RingtoneService.class);
                serviceIntent.putExtra("state", get_state);
                if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O){
                    context.startForegroundService(serviceIntent);
                } else {
                    context.startService(serviceIntent);
                }
                break;
            case "ALARM_OFF": // stopService 가 동작하지 않아서 startService 로 처리하고 나서....
                releaseCpuLock();
                Intent stopIntent = new Intent(context, RingtoneService.class);
                stopIntent.putExtra("state", get_state);
                if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O){
                    context.startForegroundService(stopIntent);
                } else {
                    context.startService(stopIntent);
                }
                break;
        }
    }

    @SuppressLint("InvalidWakeLockTag")
    private void acquireCPUWakeLock(Context context, Intent intent) {
        // 잠든 화면 깨우기
        if (wakeLock != null) {
            return;
        }
        powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
        wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK | PowerManager.ACQUIRE_CAUSES_WAKEUP
                | PowerManager.ON_AFTER_RELEASE, "WAKELOCK");
        wakeLock.acquire();
        Log.e("PushWakeLock", "Acquire cpu WakeLock = " + wakeLock);
    }

    private void releaseCpuLock() {
        Log.e("PushWakeLock", "Releasing cpu WakeLock = " + wakeLock);

        if (wakeLock != null) {
            wakeLock.release();
            wakeLock = null;
        }
    }
}


RingtoneService.java

package com.link2me.android.wakemeup;

import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.graphics.Color;
import android.media.MediaPlayer;
import android.os.Build;
import android.os.IBinder;
import android.util.Log;

import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;


public class RingtoneService extends Service{
    private static final String TAG = RingtoneService.class.getSimpleName();
    MediaPlayer mediaPlayer;

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        // Service 객체와 (화면단 Activity 사이에서) 데이터를 주고받을 때 사용하는 메서드
        // 데이터를 전달할 필요가 없으면 return null;
        return null;
    }

    @Override
    public void onCreate() {
        super.onCreate();
        // 서비스에서 가장 먼저 호출됨(최초에 한번만)
        Log.i(TAG, "RingtoneService Started");

        NotificationHelper notificationHelper = new NotificationHelper(getApplicationContext());
        notificationHelper.createNotification("알람시작","알람음이 재생됩니다.");

        // https://link2me.tistory.com/1514 에 첨부된 파일 받아서 수정 사용하면 해결됨

    }


    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.i(TAG, "onStartCommand startID === "+startId); // 계속 증가되는 값

        String getState = intent.getExtras().getString("state");
        Log.e("TAG","onStartCommand getState : " + getState);

        switch (getState) {
            case "ALARM_ON":
                if(mediaPlayer == null){
                    mediaPlayer = MediaPlayer.create(this,R.raw.ouu);
                    mediaPlayer.start();

                    mediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
                        @Override
                        public void onCompletion(MediaPlayer mp) {
                            Log.e(TAG, "mediaPlayer Completed!");
                            mediaPlayer.stop();
                            mediaPlayer.reset();
                            mediaPlayer.release();
                        }
                    });
                }
                break;
            case "ALARM_OFF":
                Log.e(TAG, "onStartCommand Stoped!");

                if (mediaPlayer != null) {
                    if(mediaPlayer.isPlaying() == true){
                        mediaPlayer.stop();
                        mediaPlayer.release(); // 자원 반환
                        mediaPlayer = null;
                    }
                }
                stopSelf();
                break;
            default:
                break;
        }
        return START_NOT_STICKY;
    }

    @Override
    public void onDestroy() {
        Log.i(TAG, "Action Service Ended");
        super.onDestroy();
        stopForeground(true);
    }
}



도움이 되셨다면 ... 해 주세요. 좋은 글 작성에 큰 힘이 됩니다.

me.tistory.com/1110 [소소한 일상 및 업무TIP 다루기]


블로그 이미지

Link2Me

,
728x90

안드로이드 배터리 잔량 정보를 표시하는 코드이다.


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/tv_battery"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:gravity="center"
        android:text="배터리 잔량"
        />

</LinearLayout>

import android.content.Intent;
import android.content.IntentFilter;
import android.os.BatteryManager;
import android.os.Bundle;
import android.widget.TextView;

import androidx.appcompat.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity {

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

        TextView textView = findViewById(R.id.tv_battery);

        // 안드로이드의 BatteryManager가 고정 인텐트를 사용하고 있기 때문에
        // 실제 BroadcastReceiver를 등록 할 필요가 없다.

        IntentFilter ifilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
        Intent batteryStatus = registerReceiver(null, ifilter);

        int level = batteryStatus.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
        int scale = batteryStatus.getIntExtra(BatteryManager.EXTRA_SCALE, -1);

        float batteryPct = level / (float)scale;
        String value = String.valueOf((int)(batteryPct*100));

        textView.setText("Battery Level Remaining: " + value + "%");
    }
}


블로그 이미지

Link2Me

,
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

,
728x90

뒤로 버튼을 두번 누르면 앱이 자동 종료되는 제대로 된 코드다.

인터넷 뒤져보면 제대로 동작하는 코드를 찾기 어렵다.

최근 앱 사용목록을 보면 종료되어 있지 않고 앱의 화면이 남이 있는 경우가 많다.

아래 코드는 최근 앱 사용목록에도 보이지 않고 종료된다.

만약 뒤로 가기 버튼 2번 누르지 않고 바로 종료하도록 하고자 한다면, appShutdown() 의 접근 제한자를 public 으로 변경하고 이 함수를 바로 호출하면 된다.


 import android.app.Activity;
import android.os.Build;
import android.widget.Toast;

public class BackPressHandler {
    private long backKeyPressedTime = 0;
    private Toast toast;

    private Activity activity;

    public BackPressHandler(Activity activity) {
        this.activity = activity;
    }

    public void onBackPressed() {

        if (isAfter2Seconds()) {
            backKeyPressedTime = System.currentTimeMillis();
            // 현재시간을 다시 초기화

            toast = Toast.makeText(activity,
                    "\'뒤로\'버튼을 한번 더 누르시면 종료됩니다.",
                    Toast.LENGTH_SHORT);
            toast.show();

            return;
        }

        if (isBefore2Seconds()) {
            appShutdown();
            toast.cancel();
        }
    }

    private Boolean isAfter2Seconds() {
        return System.currentTimeMillis() > backKeyPressedTime + 2000;
        // 2초 지났을 경우
    }

    private Boolean isBefore2Seconds() {
        return System.currentTimeMillis() <= backKeyPressedTime + 2000;
        // 2초가 지나지 않았을 경우
    }

    private void appShutdown() {
        if (Build.VERSION.SDK_INT >= 21) {
            activity.finishAndRemoveTask(); // Activity를 종료하고 최근 앱 사용 목록에서도 해당 앱을 제거
        } else {
            activity.finish();
        }
        System.runFinalization(); // 작업중인 쓰레드가 다 종료되면, 종료 시키라는 명령어
        System.exit(0); // 현재 Activity 종료
    }
}


사용법

 public class Main extends Activity {
    private BackPressHandler backPressHandler;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        backPressHandler = new BackPressHandler(this); // 뒤로 가기 버튼 이벤트

    } // onCreate End

    @Override
    public void onBackPressed() {
        backPressHandler.onBackPressed();
    }

}



블로그 이미지

Link2Me

,
728x90

안드로이드 스튜디오에서 MMS 보내는 기능을 테스트 해봤다.


MMS 직접 전송방식은 검색을 해보니 쉽게 구현할 수 있는게 아닌거 같다.

그래서 이 방식으로 보내는 테스트는 안해봤고 Intent.ACTION_SEND 방식으로 테스트를 했다.


이미지 첨부를 안한다면

sendIntent.putExtra(Intent.EXTRA_STREAM, Uri.parse(strAttachUrl));
sendIntent.setType("image/jpg");
두줄은 주석처리하면 된다.


Button mms_send = (Button) findViewById(R.id.btn_mms);
mms_send.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        String strPhone = "010-1234-5580";
        String strMessage = "전화주셔서 감사합니다.\n좋은 하루되세요.\n
            행복한 하루, 즐거운 하루, 즐거운 저녁\n
            행복하세요...\n행복은 마음에서..\n
            행복하세요...\n행복은 마음에서..\n
            행복하세요...\n행복은 마음에서..\n
            행복하세요...\n행복은 마음에서..\n
            행복하세요...\n행복은 마음에서..\n
            행복하세요...\n행복은 마음에서..";
        String strAttachUrl = "file://"+ Environment.getExternalStorageDirectory()+"/test.jpg";

        Uri uri = Uri.parse("file://"+ Environment.getExternalStorageDirectory()+"/test.jpg");
        Log.e("strAttachUrl : ", strAttachUrl);
        Log.e("imagePath : ", uri.getPath());

        Intent sendIntent = new Intent(Intent.ACTION_SEND);
        sendIntent.setClassName("com.android.mms", "com.android.mms.ui.ComposeMessageActivity");
        sendIntent.putExtra("address", strPhone);
        sendIntent.putExtra("sms_body", strMessage);
        sendIntent.putExtra(Intent.EXTRA_STREAM, Uri.parse(strAttachUrl));
        sendIntent.setType("image/jpg");
        sendIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        startActivity(sendIntent);
    }
});


로그인 처리 기능도 같이 넣어서 테스트를 하느라 AndroidManifest.xml 파일은 아래와 같다.

<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.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.SEND_MMS" />
<uses-permission android:name="android.permission.SEND_SMS" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />



앱 build.grale 에서 컴파일SDK 버전을 26으로 했더니 에러가 발생하면서 발송이 안된다.

카메라로 사진 촬영하여 발송하는 것처럼 이미지 첨부라서 안드로이드 7.0(Nougat) 이상에서 별도로 구현해 줘야 동작이 될 거 같다.

그래서 버전을 23으로 낮추고 테스트를 했더니 발송이 잘 된다.

apply plugin: 'com.android.application'

android {
    compileSdkVersion 23
    buildToolsVersion "25.0.0"

    defaultConfig {
        applicationId "com.tistory.link2me.login"
        minSdkVersion 19
        targetSdkVersion 23
        versionCode 1
        versionName "1.0"

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

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

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:support-v4:23.0.0'
    compile 'com.android.support:appcompat-v7:23.0.0'
    compile 'com.android.support.constraint:constraint-layout:1.0.2'
    compile 'com.squareup.okhttp3:okhttp:3.9.0'

}


안드로이드 7.0 이상 환경에서 컴파일하는 것도 테스트를 하게되면 적어둘 예정이다.

블로그 이미지

Link2Me

,
728x90

최초 작성일 : 2017.7.18


@Override
public boolean onKeyDown(int keyCode, KeyEvent event){
  if ( event.getAction() == KeyEvent.ACTION_DOWN ){
      if ( keyCode == KeyEvent.KEYCODE_BACK ){
          AlertDialog.Builder alert_confirm = new AlertDialog.Builder(Main.this);
          alert_confirm.setMessage("프로그램을 종료 하시겠습니까?")
          .setCancelable(false)
          .setPositiveButton("종료",
          new DialogInterface.OnClickListener() {
              @Override
              public void onClick(DialogInterface dialog, int which) {
                  // 'YES'
                  finish();
              }
          }).setNeutralButton("취소",
          new DialogInterface.OnClickListener() {
              @Override
              public void onClick(DialogInterface dialog, int which) {                 
                  //finish();
              }
          }).setNegativeButton("백그라운드",
          new DialogInterface.OnClickListener() {
              @Override
              public void onClick(DialogInterface dialog, int which) {
                  // 'No'
                  System.out.println("백그라운드 상태로 동작합니다");
                  Intent intent = new Intent();
                  intent.setAction(Intent.ACTION_MAIN);
                  intent.addCategory(Intent.CATEGORY_HOME);
                  startActivity(intent);
              return;
              }
          });
          AlertDialog alert = alert_confirm.create();
          alert.show();
      }
      if ( keyCode == KeyEvent.KEYCODE_HOME ){
         
      }
  }
  return super.onKeyDown(keyCode, event);
}


인터넷에서 찾아서 사용해본 종료에 대한 메소드다.

과연 제대로 종료가 되는지 다른 어플에서 테스트를 해보니 어플이 완전 종료되지 않았다.

finish(); : 현재 Activity 종료



Intent intent = new Intent();
intent.setAction(Intent.ACTION_MAIN);
intent.addCategory(Intent.CATEGORY_HOME);
startActivity(intent);


어플 종료라기 보다는 홈화면이 나오도록 하는 코드다.

어플이 종료되어 있는지 확인하면 종료되지 않고 남아있더라.


폭풍검색을 해서 찾아보니 어플 완전종료는 정말 쉽지 않더라.


'기본적'으로 안드로이드에는 '어플리케이션 종료' 라는 개념 자체가 없다.
한 번 실행된 프로세스는 메모리가 허락하는 한 영원히 죽지 않는다. 


http://m.blog.daum.net/creazier/15309783 에 관련 내용이 비교적 잘 정리되어 있다.

좀 오래된 내용이라 현재 기준 안드로이드에서 잘 동작하는지 여부는 아직 테스트를 못해봤다.


activity .finish(); // 현재 Activity 만 종료 처리
android.os.Process.killProcess(android.os.Process.myPid());

하면 종료될 것이라고 해서 테스트해보면 죽지 않고 좀비처럼 다시 살아난다. ㅠㅠ

Activity 가 1개만 떠있는 경우에만 정상적으로 죽는다고 하던데, 다른 Activity 를 실행하지 않은 상태에서도 테스트해보면 죽지 않고 살아 있더라.


어플 완전 종료가 되는 걸 찾게되면 수정해서 작성하련다. (2017.8.23)


드디어 완전하게 처리되는 걸 찾아냈다. (2017.8.31)

정말 우연히 Audio Player 종료처리하는 걸 테스트하다가 완전 종료되는 걸 확인했다.


if (Build.VERSION.SDK_INT >= 21)
    finishAndRemoveTask();
else
    finish();
System.exit(0);


안드로이드 스튜디오에서 컴파일하는 API 가 21보다 큰 경우인데 계속 예전 버전 종료인 finish(); 로만 처리를 하고 있었던 거다.

위 코드를 넣고 처리하니까 화면상에 남아있지 않았다.


finishAffinity()
Finish this activity as well as all activities immediately below it in the current task that have the same affinity.

finishAndRemoveTask()
Call this when your activity is done and should be closed and the task should be completely removed as a part of finishing the root activity of the task.

블로그 이미지

Link2Me

,