ともちゃんのAndroid開発日記

組込みC言語プログラマだったともちゃんが、四苦八苦しながら、AndroidのJAVAとKotlinを習得して行きます。ともちゃんの備忘録も兼ねています。

タッチに追従してViewを動かす

今までどうやったらいいかわからなかったのですが、意外に簡単でした。

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.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">

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/HelloWorld"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />

</android.support.constraint.ConstraintLayout>

 MainActivity.java

public class MainActivity extends AppCompatActivity {

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

TextView helloWorld = findViewById(R.id.HelloWorld);
helloWorld.setOnTouchListener(new moveOnTouchListener());

}

public static class moveOnTouchListener implements View.OnTouchListener {

private static float dX;
private static float dY;

@Override
public boolean onTouch(View view, MotionEvent motionEvent) {


switch (motionEvent.getAction()) {
case MotionEvent.ACTION_DOWN :
dX = view.getX() - motionEvent.getRawX();
dY = view.getY() - motionEvent.getRawY();
break;
case MotionEvent.ACTION_MOVE :
view.setY(motionEvent.getRawY() + dY);
view.setX(motionEvent.getRawX() + dX);
break;
case MotionEvent.ACTION_UP :
break;
}

return true;
}

}
}

 

OAuth2認証を使用して、添付ファイル付きGmailを自動送信する

OAuth2認証を使用して、添付ファイル付きGmailを自動送信しようとしたとき、かなりつまづいたので、記事にします。

1.Javamail-androidのダウンロード

activation.jar、additionnal.jar、mail.jarをダウンロードし、libsフォルダに入れます。

2.uses-permissions

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
<uses-permission android:name="android.permission.USE_CREDENTIALS" />

3.AuthTokenの取得

private void authenticate() {
final AccountManager manager = AccountManager.get(AuthenticationActivity.this);
final SharedPreferences prefs3 = getSharedPreferences("setting3", Context.MODE_PRIVATE);
final SharedPreferences.Editor editor3 = prefs3.edit();

String previousAuthToken = prefs3.getString("AuthToken", null);
if (previousAuthToken != null) {
manager.invalidateAuthToken("com.google", previousAuthToken);
}
manager.getAuthToken(new Account(mailAddressFrom, "com.google"),
"oauth2:https://mail.google.com/",
null,
false,
new AccountManagerCallback<Bundle>() {
@Override
public void run(AccountManagerFuture<Bundle> future) {
try {
Bundle bundle = future.getResult();
Intent intent = (Intent) bundle.get(AccountManager.KEY_INTENT);
// 認証されていない場合は認証許可ダイアログを表示
if (intent != null) {
startActivityForResult(intent, REQUEST_AUTH_DIALOG);
return;
}
String authToken =
bundle.getString(AccountManager.KEY_AUTHTOKEN);
editor3.putString("AuthToken", authToken);
editor3.apply();
finish();
} catch (OperationCanceledException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (AuthenticatorException e) {
e.printStackTrace();
}
}
},
null);

}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (resultCode == Activity.RESULT_OK) {
switch (requestCode) {
case REQUEST_AUTH_DIALOG:
authenticate();
break;
}

}
}

4.Gmailの送信

public class GmailSendHelper {
private static final int RESPONSE_CODE_AUTH_SUCCESS = 235;
private static final int SUBMISSION_PORT = 587;
private static final String GMAIL_SMTP_SERVER = "smtp.gmail.com";
private final String mailAddressFrom;
private final String authToken;
private Session session;

public GmailSendHelper(String mailAddressFrom, String authToken) {
this.mailAddressFrom = mailAddressFrom;
this.authToken = authToken;
}

private SMTPTransport connectToSmtp(String host, int port)
throws MessagingException {
Properties props = new Properties();
props.put("mail.smtp.starttls.enable", "true");
props.put("mail.smtp.starttls.required", "true");
props.put("mail.smtp.sasl.enable", "false");
// props.put("mail.smtp.ssl.enable", "true");
session = Session.getInstance(props);

SMTPTransport transport = new SMTPTransport(session, /* URL Name */ null);
transport.connect(host, port, mailAddressFrom,
/* Password, OAuth認証なのでNull */ null);

byte[] response = String.format("user=%s\1auth=Bearer %s\1\1", mailAddressFrom,
authToken).getBytes();
response = BASE64EncoderStream.encode(response);

transport.issueCommand(
"AUTH XOAUTH2 " + new String(response), RESPONSE_CODE_AUTH_SUCCESS);
return transport;
}

public void sendEmail(
InternetAddress[] internetAddresses,
ReportFragment.ReportMessageAndFile resultOfExport
)
throws MessagingException {
SMTPTransport smtpTransport
= connectToSmtp(GMAIL_SMTP_SERVER, SUBMISSION_PORT);

MimeMessage mimeMsg = new MimeMessage(session);

try {

mimeMsg.setSubject(resultOfExport.mailSubject, "utf-8");
mimeMsg.setFrom(new InternetAddress(mailAddressFrom));
mimeMsg.setRecipients(javax.mail.Message.RecipientType.TO,
internetAddresses);

// メール本文
final MimeBodyPart txtPart = new MimeBodyPart();
txtPart.setText(resultOfExport.mailText, "utf-8");

// 添付ファイル
final MimeBodyPart filePart = new MimeBodyPart();
File file = new File(resultOfExport.filePath);
FileDataSource fds = new FileDataSource(file);
DataHandler data = new DataHandler(fds);
filePart.setDataHandler(data);
filePart.setFileName(MimeUtility.encodeWord(resultOfExport.fileName));

final Multipart mp = new MimeMultipart();
mp.addBodyPart(txtPart);
mp.addBodyPart(filePart);
mimeMsg.setContent(mp);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}

smtpTransport.sendMessage(mimeMsg, mimeMsg.getAllRecipients());
}
 

5.API ConsoleでクライアントIDを登録する。

https://console.developers.google.com/apis/credentials

で認証情報を獲得します。「認証情報を作成」→「ウィザードで選択」を選択します。

f:id:uchida001tmhr:20180630072638p:plain

f:id:uchida001tmhr:20180630072829p:plain

名前:適当な名前を付けます。

名証明書フィンガープリント(デバッグ時:WIndows):

keytool -exportcert -alias androiddebugkey -keystore "%USERPROFILE%\.android\debug.keystore" -list -v

名証明書フィンガープリント(リリース時:WIndows):

keytool -exportcert -keystore [jksファイルのフルパス] -list -v

 上記を実行:

f:id:uchida001tmhr:20180630073631p:plain

パスワード(デバッグ時):android

パスワード(リリース時):jksファイルに設定したパスワード

 

SHA1のフィンガープリントをコピーして、上記「OAuthクライアントIDの作成」に貼り付ける。

 

これで、コンパイルデバッグすれば、自動でOAuth2認証を行い、添付メール付きメールを自動送信してくれます。

 

progressDialogがAPI26で非推奨になったため、代替でProgressBarつきSnackbarを実装してみた

Kotlinのソースです。

snackBar = Snackbar.make(dataCenter.getImportView()!!, "", Snackbar.LENGTH_INDEFINITE)
val snackView = snackBar!!.view as Snackbar.SnackbarLayout
progressBar = ProgressBar(context, null, android.R.attr.progressBarStyleHorizontal)
snackView.addView(progressBar)
snackBar!!.show()

 ・・・

progressBar!!.max = length
・・・
progressBar.progress = i+1
・・・
if (snackBar!!.isShown) {
snackBar!!.dismiss()
}

 これで水平タイプのProgressBarをSnackbarに表示できます。

 

Javaでの実装は、こんな感じ。(スピナータイプの例)

snackbar = Snackbar.make(dataCenter.getViewSetting(), res.getString(R.string.process_in_progress), Snackbar.LENGTH_INDEFINITE);
Snackbar.SnackbarLayout snackView = (Snackbar.SnackbarLayout) snackbar.getView();
progressBar = new ProgressBar(context, null, android.R.attr.progressBarStyleSmall);
snackView.addView(progressBar);
snackbar.show();

・・・

if (snackbar.isShown()) {
snackbar.dismiss();
}

 

 

排他制御(synchronizedブロック)

共通資源を排他制御するには、synchronizedブロックを使います。

synchronized(共通資源のインスタンス) {
共通資源へのアクセス
}

 私の実装例は以下です。(Kotlinのサンプルコード)

synchronized(databaseManager.getSwitchingListDatabase()) {
cursor = databaseManager.querySwitchingListDatabase()
}

 

Android 8.0の通知(NotificationChannel)

Android 8.0では、通知にNotificationChannelを設定してやる必要があります。

NotificationManager myNotification = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel(DEFAULT_CHANNEL,
resources.getString(R.string.notification_medicine_time),
NotificationManager.IMPORTANCE_DEFAULT);
/* 指定できる値は以下7種類。
NotificationManager.IMPORTANCE_UNSPECIFIED (-1000);
NotificationManager.IMPORTANCE_NONE (0);
NotificationManager.IMPORTANCE_MIN (1);
NotificationManager.IMPORTANCE_LOW (2);
NotificationManager.IMPORTANCE_DEFAULT (3);
NotificationManager.IMPORTANCE_HIGH (4);
NotificationManager.IMPORTANCE_MAX (5);
*/
myNotification.createNotificationChannel(channel);
}
Intent intent = new Intent(context, TakingCheckActivity.class);
intent.putExtra("user", user_id);
intent.putExtra("taking_timing", taking_timing);
intent.putExtra("time_in_millis", time_in_millis);
intent.setType(message);
Intent bootIntent[] = {intent};
PendingIntent contentIntent = PendingIntent.getActivities(context, notification_id, bootIntent, PendingIntent.FLAG_UPDATE_CURRENT);
Notification.Builder builder;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
builder = new Notification.Builder(context, DEFAULT_CHANNEL);
} else {
builder = new Notification.Builder(context);
}
builder.setSmallIcon(R.drawable.notification_small_icon)
.setLargeIcon(BitmapFactory.decodeResource(context.getResources(), R.drawable.notification_large_icon))
.setContentTitle(resources.getString(R.string.notification_message_1))
.setContentText(message)
.setWhen(System.currentTimeMillis())
.setPriority(Notification.PRIORITY_DEFAULT)
.setAutoCancel(true)
.setSound(uri)
.setDefaults(Notification.DEFAULT_LIGHTS)
.setContentIntent(contentIntent);

myNotification.notify(notification_id, builder.build());

 

Android 8.0のバックグラウンド処理

Android 8.0では、バックグラウンド処理に厳しい制約が付けられました。

よって、バックグラウンド処理からフォアグラウンド処理に移行するソースコードです。

 

サービスを呼び出す側

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
activity.startForegroundService(intent)
} else {
activity.startService(intent)
}

 

サービス側

@TargetApi(Build.VERSION_CODES.O)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {

val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val channel = NotificationChannel(DEFAULT_CHANNEL, getString(R.string.notification_channel), NotificationManager.IMPORTANCE_NONE)
/** 指定できる値は以下7種類。
NotificationManager.IMPORTANCE_UNSPECIFIED (-1000);
NotificationManager.IMPORTANCE_NONE (0);
NotificationManager.IMPORTANCE_MIN (1);
NotificationManager.IMPORTANCE_LOW (2);
NotificationManager.IMPORTANCE_DEFAULT (3);
NotificationManager.IMPORTANCE_HIGH (4);
NotificationManager.IMPORTANCE_MAX (5);
**/
notificationManager.createNotificationChannel(channel)

}

val intent = Intent(applicationContext, MainActivity::class.java)
val intents = arrayOf(intent)
val pendingIntent = PendingIntent.getActivities(applicationContext, 0, intents, PendingIntent.FLAG_UPDATE_CURRENT)
val builder = NotificationCompat.Builder(applicationContext, DEFAULT_CHANNEL)
builder.setContentIntent(pendingIntent)
builder.setContentTitle(resources.getString(R.string.app_name))
builder.setContentText(resources.getString(R.string.please_tap_here))
builder.setWhen(System.currentTimeMillis())
builder.setAutoCancel(false)
builder.setSmallIcon(R.drawable.notification_small)
builder.setLargeIcon(BitmapFactory.decodeResource(resources, R.drawable.notification_large))

val notification = builder.build()
notification.flags = Notification.FLAG_NO_CLEAR

startForeground(1, notification)

startForegroundService()でサービスを起動してから、5秒以内にstartForeground()を呼び出す必要があります。

 

端末がスリープしても動き続けるバックグラウンド処理

HandlerとWakeLockで実現しました。(ソースコードはKotlinで書いています。)

 

AndroidManifest.xml

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

 

 時間を刻むタイマーサービス

TimerService.kt

class TimerService : Service() {

val handler = Handler()

companion object {
private var wakelock : PowerManager.WakeLock? = null
private var timer : Timer? = null
}

override fun onBind(intent: Intent): IBinder? {
return null
}

override fun onCreate() {
super.onCreate()

val databaseManager = DatabaseManager()
databaseManager.openDatabaseAll(applicationContext)

val myNotification = MyNotification(applicationContext)
myNotification.displayNotification()

val powerManager = applicationContext!!.getSystemService(Context.POWER_SERVICE) as PowerManager
wakelock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "PartialWakelock")
wakelock!!.acquire()

}

override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)

var count = 0L
var timePrevious : Long
var timeNow = System.currentTimeMillis()
var timeDifference : Long

handler.postDelayed(object : Runnable {
override fun run() {
OnOffSetter() // タイマー内の処理
handler.postDelayed(this, 15*1000)

timePrevious = timeNow
timeNow = System.currentTimeMillis()
timeDifference = timeNow - timePrevious
count++
Log.i("TimerService", count.toString()+"/"+timeDifference.toString())

}
}, 15*1000)

return START_STICKY
}

override fun onDestroy() {
super.onDestroy()

handler.removeCallbacksAndMessages(null)

val myNotification = MyNotification(applicationContext)
myNotification.cancelNotification()

if (wakelock != null) {
wakelock!!.release()
}

}
}

 

 タイマーを起動する処理

val intent = Intent(this, TimerService::class.java)
startService(intent)

stopService(intent)

 

 

WakeLockは、バッテリーを消費するので、必要最低限にする必要があります。