MPAndroidChartでLineChartを描く
MPAndroidChartでLineChartを描くのに苦労しましたので、サンプルコードを載せておきます。
private class GraphManager {
private LinearLayout chartLayout;
private final Context context;
public GraphManager(Context context) {
this.context = context;
}
@SuppressWarnings("ConstantConditions")
public void displayGraph(
String legend,
int dataCount,
Date[] dateValue,
boolean[] flagActual,
double[] yActualValue,
boolean[] flagActualNight,
double[] yActualValueNight,
boolean[] flagTarget,
double[] yTargetValue) {
Resources resources = getResources();
DataCenter dataCenter = new DataCenter();
View view = dataCenter.getGraphView();
LineChart lineChart = view.findViewById(R.id.chartArea);
lineChart.getAxisRight().setEnabled(false);
lineChart.getAxisLeft().setEnabled(true);
lineChart.setDrawGridBackground(true);
lineChart.setTouchEnabled(false);
lineChart.setPinchZoom(false);
lineChart.setDoubleTapToZoomEnabled(false);
lineChart.setScaleEnabled(true);
lineChart.getLegend().setEnabled(true);
lineChart.getDescription().setText("");
//X軸周り
XAxis xAxis = lineChart.getXAxis();
xAxis.setDrawLabels(true);
xAxis.setPosition(XAxis.XAxisPosition.BOTTOM);
xAxis.setDrawGridLines(true);
YAxis yAxis = lineChart.getAxisLeft();
yAxis.setLabelCount(10);
lineChart.setData(createLineChartData(
legend,
xAxis,
dataCount,
dateValue,
flagActual,
yActualValue,
flagActualNight,
yActualValueNight,
flagTarget,
yTargetValue
));
lineChart.invalidate();
}
private LineData createLineChartData(
String legend,
XAxis xAxis,
int dataCount,
Date[] dateValue,
boolean[] existFlagActual,
double[] yActualValue,
boolean[] existFlagActualNight,
double[] yActualValueNight,
boolean[] existFlagTarget,
double[] yTargetValue) {
xAxis.setValueFormatter(new XAxisValueFormatter(dateValue));
Point point = getDisplaySize(getActivity());
reverseOrderDate(dateValue);
reverseOrder(yActualValue, existFlagActual);
reverseOrder(yActualValueNight, existFlagActualNight);
reverseOrder(yTargetValue, existFlagTarget);
ArrayList<Entry> actualValues = new ArrayList<>();
for (int i=0; i<existFlagActual.length; i++) {
if (existFlagActual[i]) {
actualValues.add(new Entry((float)i, (float)yActualValue[i]));
}
}
ArrayList<Entry> actualValuesNight = new ArrayList<>();
for (int i=0; i<existFlagActualNight.length; i++) {
if (existFlagActualNight[i]) {
actualValuesNight.add(new Entry((float)i, (float)yActualValueNight[i]));
}
}
ArrayList<Entry> targetValues = new ArrayList<>();
for (int i=0; i<existFlagTarget.length; i++) {
if (existFlagTarget[i]) {
targetValues.add(new Entry((float)i, (float)yTargetValue[i]));
}
}
List<ILineDataSet> dataSets = new ArrayList<>();
LineDataSet actualTargetDataSet = new LineDataSet(targetValues, legend + "(" + getString(R.string.graph_target) + ")");
actualTargetDataSet.setColor(Color.rgb(180, 0, 180));
actualTargetDataSet.setCircleColor(Color.rgb(180, 0, 180));
actualTargetDataSet.setLineWidth(2f);
actualTargetDataSet.setDrawValues(false);
dataSets.add(actualTargetDataSet);
LineDataSet actualDataSet = new LineDataSet(actualValues, legend + "(" + getString(R.string.graph_legend_morning) + ")");
actualDataSet.setColor(Color.rgb(0x4d, 0xb6, 0xac));
actualDataSet.setCircleColor(Color.rgb(0x4d, 0xb6, 0xac));
actualDataSet.setLineWidth(2f);
if (DISPLAY_PERIOD_DAY == DISPLAY_PERIOD_14DAYS && RICH_INFO) {
actualDataSet.setDrawValues(true);
actualDataSet.setValueTextSize(point.x/(120.0f));
actualDataSet.setValueTextColor(Color.rgb(0x4d, 0xb6, 0xac));
} else {
actualDataSet.setDrawValues(false);
}
dataSets.add(actualDataSet);
LineDataSet actualNightDataSet = new LineDataSet(actualValuesNight, legend + "(" + getString(R.string.graph_legend_night) + ")");
actualNightDataSet.setColor(Color.rgb(255, 166, 0));
actualNightDataSet.setCircleColor(Color.rgb(255, 166, 0));
actualNightDataSet.setLineWidth(2f);
if (DISPLAY_PERIOD_DAY == DISPLAY_PERIOD_14DAYS && RICH_INFO) {
actualNightDataSet.setDrawValues(true);
actualNightDataSet.setValueTextSize(point.x / (120.0f));
actualNightDataSet.setValueTextColor(Color.rgb(255, 166, 0));
} else {
actualNightDataSet.setDrawValues(false);
}
dataSets.add(actualNightDataSet);
return new LineData(dataSets);
}
}
public class XAxisValueFormatter extends ValueFormatter {
Date[] mDateValues;
public XAxisValueFormatter(Date[] dateValues) {
mDateValues = dateValues;
}
@Override
public String getFormattedValue(float value) {
// write your logic here
int val = (int) value;
SimpleDateFormat sdf = new SimpleDateFormat("MM/dd",Locale.getDefault());
if (mDateValues[val] != null) {
return sdf.format(mDateValues[val]);
} else {
return String.valueOf(value);
}
}
}
private void reverseOrderDate(Date[] date) {
int size = date.length;
for (int i=0; i<size/2; i++) {
Date temp1 = date[i];
date[i] = date[size-1-i];
date[size-1-i] = temp1;
}
}
private void reverseOrder(double[] values, boolean[] existFlag) {
int size = values.length;
for (int i=0; i<size/2; i++) {
double temp1 = values[i];
values[i] = values[size-1-i];
values[size-1-i] = temp1;
boolean temp2 = existFlag[i];
existFlag[i] = existFlag[size-1-i];
existFlag[size-1-i] = temp2;
}
}
@SuppressWarnings("ConstantConditions")
private class MakeAndDisplayGraph {
public MakeAndDisplayGraph() {
}
//日付時刻文字列を Date型に変換
private Date String2date(String strDate) {
Date dateDate = null;
// 日付文字列→date型変換フォーマットを指定して
SimpleDateFormat sdf1 = new SimpleDateFormat("yyyy/M/d", Locale.JAPAN);
try {
dateDate = sdf1.parse(strDate);
} catch (ParseException e) {
Resources resources = getResources();
Toast.makeText(getActivity(), resources.getString(R.string.graph_message_input_correct_date), Toast.LENGTH_SHORT).show();
}
return dateDate;
}
public void makeAndDisplayGraph(CalendarAction action, int checkedId) {
Resources resources = getResources();
final String strDateMin = "2200/12/31";
final String strDateMax = "1970/1/1";
DatabaseManager databaseManager = new DatabaseManager();
Cursor cursor = databaseManager.queryDatabaseAll();
Date dateMin = String2date(strDateMin);
Date dateMax = String2date(strDateMax);
if (cursor.moveToFirst()) {
do {
Date date = String2date(cursor.getString(cursor.getColumnIndex("date")));
if (date.getTime() < dateMin.getTime()) {
dateMin = date;
}
if (dateMax.getTime() < date.getTime()) {
dateMax = date;
}
} while (cursor.moveToNext());
}
cursor.close();
DataCenter dataCenter = new DataCenter();
Calendar cal_start = dataCenter.getCalendarGraph();
Calendar cal_now = Calendar.getInstance();
switch (action) {
case NEXT:
cal_start.add(Calendar.DATE, DISPLAY_PERIOD_DAY);
if (cal_now.getTime().getTime() <= cal_start.getTime().getTime()) {
dataCenter.newCalendarGraph();
cal_start = dataCenter.getCalendarGraph();
Toast.makeText(getActivity(),resources.getString(R.string.graph_message_display_latest_data),Toast.LENGTH_SHORT).show();
}
break;
case PREVIOUS:
cal_start.add(Calendar.DATE, -(DISPLAY_PERIOD_DAY));
break;
case TODAY:
cal_start = dataCenter.newCalendarGraph();
break;
}
switch (checkedId) {
case R.id.radioButtonWeight: {
Date[] dateValue = new Date[DISPLAY_PERIOD_DAY];
double[] yActualValue = new double[DISPLAY_PERIOD_DAY];
double[] yActualValueNight = new double[DISPLAY_PERIOD_DAY];
double[] yTargetValue = new double[DISPLAY_PERIOD_DAY];
boolean[] existFlagActual = new boolean[DISPLAY_PERIOD_DAY];
boolean[] existFlagActualNight = new boolean[DISPLAY_PERIOD_DAY];
boolean[] existFlagTarget = new boolean[DISPLAY_PERIOD_DAY];
int year = cal_start.get(Calendar.YEAR);
int month = cal_start.get(Calendar.MONTH) + 1;
int date = cal_start.get(Calendar.DATE);
Calendar cal_index = Calendar.getInstance();
cal_index.set(Calendar.YEAR, year);
cal_index.set(Calendar.MONTH, month - 1);
cal_index.set(Calendar.DATE, date);
boolean exist_flag = false;
int dataCount = 0;
for (int i = 0; i < DISPLAY_PERIOD_DAY; i++) {
String strYearMonthDate = String.valueOf(year) + "/" + String.valueOf(month) + "/" + String.valueOf(date);
cursor = databaseManager.queryDatabase(strYearMonthDate);
dateValue[dataCount] = String2date(strYearMonthDate);
if (cursor.moveToFirst()) {
do {
double actualValue = cursor.getDouble(cursor.getColumnIndex("actual_weight"));
double actualValueNight = cursor.getDouble(cursor.getColumnIndex("actual_weight_night"));
double targetValue = cursor.getDouble(cursor.getColumnIndex("target_weight"));
yActualValue[dataCount] = actualValue;
if (actualValue != 0f) {
existFlagActual[dataCount] = true;
exist_flag = true;
} else {
existFlagActual[dataCount] = false;
}
yActualValueNight[dataCount] = actualValueNight;
if (actualValueNight != 0f) {
existFlagActualNight[dataCount] = true;
exist_flag = true;
} else {
existFlagActualNight[dataCount] = false;
}
yTargetValue[dataCount] = targetValue;
if (targetValue != 0f) {
existFlagTarget[dataCount] = true;
exist_flag = true;
} else {
existFlagTarget[dataCount] = false;
}
} while (cursor.moveToNext());
}
cursor.close();
cal_index.add(Calendar.DATE, -1);
year = cal_index.get(Calendar.YEAR);
month = cal_index.get(Calendar.MONTH) + 1;
date = cal_index.get(Calendar.DATE);
dataCount++;
}
Calendar cal_end = Calendar.getInstance();
cal_end.set(Calendar.YEAR, year);
cal_end.set(Calendar.MONTH, month - 1);
cal_end.set(Calendar.DATE, date);
cal_end.add(Calendar.DATE, 1);
if (exist_flag) {
GraphManager graphManager = new GraphManager(getActivity());
graphManager.displayGraph(
resources.getString(R.string.graph_weight),
dataCount,
dateValue,
existFlagActual,
yActualValue,
existFlagActualNight,
yActualValueNight,
existFlagTarget,
yTargetValue);
} else {
Toast.makeText(getActivity(), resources.getString(R.string.graph_message_no_data), Toast.LENGTH_SHORT).show();
switch (action) {
case NEXT:
if (dateMax.getTime() <= cal_start.getTime().getTime()) {
cal_start.add(Calendar.DATE, -(DISPLAY_PERIOD_DAY));
}
break;
case PREVIOUS:
if (cal_end.getTime().getTime() <= dateMin.getTime()) {
cal_start.add(Calendar.DATE, DISPLAY_PERIOD_DAY);
}
break;
}
}
}
break;
case R.id.radioButtonBodyFatPercentage: {
Date[] dateValue = new Date[DISPLAY_PERIOD_DAY];
double[] yActualValue = new double[DISPLAY_PERIOD_DAY];
double[] yActualValueNight = new double[DISPLAY_PERIOD_DAY];
double[] yTargetValue = new double[DISPLAY_PERIOD_DAY];
boolean[] existFlagActual = new boolean[DISPLAY_PERIOD_DAY];
boolean[] existFlagActualNight = new boolean[DISPLAY_PERIOD_DAY];
boolean[] existFlagTarget = new boolean[DISPLAY_PERIOD_DAY];
int year = cal_start.get(Calendar.YEAR);
int month = cal_start.get(Calendar.MONTH) + 1;
int date = cal_start.get(Calendar.DATE);
Calendar cal_index = Calendar.getInstance();
cal_index.set(Calendar.YEAR, year);
cal_index.set(Calendar.MONTH, month - 1);
cal_index.set(Calendar.DATE, date);
boolean exist_flag = false;
int dataCount = 0;
for (int i = 0; i < DISPLAY_PERIOD_DAY; i++) {
String strYearMonthDate = String.valueOf(year) + "/" + String.valueOf(month) + "/" + String.valueOf(date);
cursor = databaseManager.queryDatabase(strYearMonthDate);
dateValue[dataCount] = String2date(strYearMonthDate);
if (cursor.moveToFirst()) {
do {
double actualValue = cursor.getDouble(cursor.getColumnIndex("actual_body_fat_percentage"));
double actualValueNight = cursor.getDouble(cursor.getColumnIndex("actual_body_fat_percentage_night"));
double targetValue = cursor.getDouble(cursor.getColumnIndex("target_body_fat_percentage"));
yActualValue[dataCount] = actualValue;
if (actualValue != 0f) {
existFlagActual[dataCount] = true;
exist_flag = true;
} else {
existFlagActual[dataCount] = false;
}
yActualValueNight[dataCount] = actualValueNight;
if (actualValueNight != 0f) {
existFlagActualNight[dataCount] = true;
exist_flag = true;
} else {
existFlagActualNight[dataCount] = false;
}
yTargetValue[dataCount] = targetValue;
if (targetValue != 0f) {
existFlagTarget[dataCount] = true;
exist_flag = true;
} else {
existFlagTarget[dataCount] = false;
}
} while (cursor.moveToNext());
}
cursor.close();
cal_index.add(Calendar.DATE, -1);
year = cal_index.get(Calendar.YEAR);
month = cal_index.get(Calendar.MONTH) + 1;
date = cal_index.get(Calendar.DATE);
dataCount++;
}
Calendar cal_end = Calendar.getInstance();
cal_end.set(Calendar.YEAR, year);
cal_end.set(Calendar.MONTH, month - 1);
cal_end.set(Calendar.DATE, date);
cal_end.add(Calendar.DATE, 1);
if (exist_flag) {
GraphManager graphManager = new GraphManager(getActivity());
graphManager.displayGraph(
resources.getString(R.string.graph_body_fat_percentage),
dataCount,
dateValue,
existFlagActual,
yActualValue,
existFlagActualNight,
yActualValueNight,
existFlagTarget,
yTargetValue);
} else {
Toast.makeText(getActivity(), resources.getString(R.string.graph_message_no_data), Toast.LENGTH_SHORT).show();
switch (action) {
case NEXT:
if (dateMax.getTime() <= cal_start.getTime().getTime()) {
cal_start.add(Calendar.DATE, -(DISPLAY_PERIOD_DAY));
}
break;
case PREVIOUS:
if (cal_end.getTime().getTime() <= dateMin.getTime()) {
cal_start.add(Calendar.DATE, DISPLAY_PERIOD_DAY);
}
break;
}
}
}
break;
case R.id.radioButtonBMI: {
Date[] dateValue = new Date[DISPLAY_PERIOD_DAY];
double[] yActualValue = new double[DISPLAY_PERIOD_DAY];
double[] yActualValueNight = new double[DISPLAY_PERIOD_DAY];
double[] yTargetValue = new double[DISPLAY_PERIOD_DAY];
boolean[] existFlagActual = new boolean[DISPLAY_PERIOD_DAY];
boolean[] existFlagActualNight = new boolean[DISPLAY_PERIOD_DAY];
boolean[] existFlagTarget = new boolean[DISPLAY_PERIOD_DAY];
int year = cal_start.get(Calendar.YEAR);
int month = cal_start.get(Calendar.MONTH) + 1;
int date = cal_start.get(Calendar.DATE);
Calendar cal_index = Calendar.getInstance();
cal_index.set(Calendar.YEAR, year);
cal_index.set(Calendar.MONTH, month - 1);
cal_index.set(Calendar.DATE, date);
boolean exist_flag = false;
int dataCount = 0;
for (int i = 0; i < DISPLAY_PERIOD_DAY; i++) {
String strYearMonthDate = String.valueOf(year) + "/" + String.valueOf(month) + "/" + String.valueOf(date);
cursor = databaseManager.queryDatabase(strYearMonthDate);
dateValue[dataCount] = String2date(strYearMonthDate);
if (cursor.moveToFirst()) {
do {
double actualValue = cursor.getDouble(cursor.getColumnIndex("actual_BMI"));
double actualValueNight = cursor.getDouble(cursor.getColumnIndex("actual_BMI_night"));
double targetValue = cursor.getDouble(cursor.getColumnIndex("target_BMI"));
yActualValue[dataCount] = actualValue;
if (actualValue != 0f) {
existFlagActual[dataCount] = true;
exist_flag = true;
} else {
existFlagActual[dataCount] = false;
}
yActualValueNight[dataCount] = actualValueNight;
if (actualValueNight != 0f) {
existFlagActualNight[dataCount] = true;
exist_flag = true;
} else {
existFlagActualNight[dataCount] = false;
}
yTargetValue[dataCount] = targetValue;
if (targetValue != 0f) {
existFlagTarget[dataCount] = true;
exist_flag = true;
} else {
existFlagTarget[dataCount] = false;
}
} while (cursor.moveToNext());
}
cursor.close();
cal_index.add(Calendar.DATE, -1);
year = cal_index.get(Calendar.YEAR);
month = cal_index.get(Calendar.MONTH) + 1;
date = cal_index.get(Calendar.DATE);
dataCount++;
}
Calendar cal_end = Calendar.getInstance();
cal_end.set(Calendar.YEAR, year);
cal_end.set(Calendar.MONTH, month - 1);
cal_end.set(Calendar.DATE, date);
cal_end.add(Calendar.DATE, 1);
if (exist_flag) {
GraphManager graphManager = new GraphManager(getActivity());
graphManager.displayGraph(
resources.getString(R.string.graph_BMI),
dataCount,
dateValue,
existFlagActual,
yActualValue,
existFlagActualNight,
yActualValueNight,
existFlagTarget,
yTargetValue);
} else {
Toast.makeText(getActivity(), resources.getString(R.string.graph_message_no_data), Toast.LENGTH_SHORT).show();
switch (action) {
case NEXT:
if (dateMax.getTime() <= cal_start.getTime().getTime()) {
cal_start.add(Calendar.DATE, -(DISPLAY_PERIOD_DAY));
}
break;
case PREVIOUS:
if (cal_end.getTime().getTime() <= dateMin.getTime()) {
cal_start.add(Calendar.DATE, DISPLAY_PERIOD_DAY);
}
break;
}
}
}
break;
}
}
}
実行結果はこんな感じです。
Android 8.0の通知ドット(通知バッジ)を削除する。
Android 8.0の通知ドット(通知バッジ)が邪魔な時があるので、削除する設定です。
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val channel = NotificationChannel(MAIN_CHANNEL, getString(R.string.main_channel), NotificationManager.IMPORTANCE_LOW)
/** 指定できる値は以下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);
**/
channel.setShowBadge(false)
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, MAIN_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_main_small)
builder.setLargeIcon(BitmapFactory.decodeResource(resources, R.drawable.notification_main_large))
builder.setBadgeIconType(NotificationCompat.BADGE_ICON_NONE)
val notification = builder.build()
startForeground(NOTIFICATION_ID, notification)
上記の
channel.setShowBadge(false)
と
builder.setBadgeIconType(NotificationCompat.BADGE_ICON_NONE)
で、通知ドット(通知バッジ)を消しています。
画像の自動リネーム
DSC auto renameに触発されて、どうやったらこのようなアプリが作れるのかと思い、PM Rename(Photo/Movie Rename)を作りました。
1. AndroidManifest.xml
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
リネームサービスを起動するので、
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
が必要です。
また、ストレージのファイル名を変更するので、
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
が必要です。
2. MainSerivce
写真が撮られたことの監視は、Content Ovserverを使います。そのContent Observerの登録です。
fun regContentObserver(context: Context) {
val uriPhotoIn = MediaStore.Images.Media.INTERNAL_CONTENT_URI
val uriPhotoEx = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
val uriMovieIn = MediaStore.Video.Media.INTERNAL_CONTENT_URI
val uriMovieEx = MediaStore.Video.Media.EXTERNAL_CONTENT_URI
contentObserverPhotoIn = object : ContentObserver(Handler()) {
override fun onChange(selfChange: Boolean) {
super.onChange(selfChange)
val prefs = context.getSharedPreferences("Setting", Context.MODE_PRIVATE)
val delay = prefs.getInt("delay", 0)
registerAlarmPhotoIn(context, delay)
}
}
contentObserverPhotoEx = object : ContentObserver(Handler()) {
override fun onChange(selfChange: Boolean) {
super.onChange(selfChange)
val prefs = context.getSharedPreferences("Setting", Context.MODE_PRIVATE)
val delay = prefs.getInt("delay", 0)
registerAlarmPhotoEx(context, delay)
}
}
contentObserverMovieIn = object : ContentObserver(Handler()) {
override fun onChange(selfChange: Boolean) {
super.onChange(selfChange)
val prefs = context.getSharedPreferences("Setting", Context.MODE_PRIVATE)
val delay = prefs.getInt("delay", 0)
registerAlarmMovieIn(context, delay)
}
}
contentObserverMovieEx = object : ContentObserver(Handler()) {
override fun onChange(selfChange: Boolean) {
super.onChange(selfChange)
val prefs = context.getSharedPreferences("Setting", Context.MODE_PRIVATE)
val delay = prefs.getInt("delay", 0)
registerAlarmMovieEx(context, delay)
}
}
context.contentResolver.registerContentObserver(uriPhotoIn, true, contentObserverPhotoIn)
context.contentResolver.registerContentObserver(uriPhotoEx, true, contentObserverPhotoEx)
context.contentResolver.registerContentObserver(uriMovieIn, true, contentObserverMovieIn)
context.contentResolver.registerContentObserver(uriMovieEx, true, contentObserverMovieEx)
}
これを、Main Serviceから呼びます。
override fun onCreate() {
super.onCreate()
@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(MAIN_CHANNEL, getString(R.string.main_channel), NotificationManager.IMPORTANCE_LOW)
/** 指定できる値は以下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, MAIN_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_main_small)
builder.setLargeIcon(BitmapFactory.decodeResource(resources, R.drawable.notification_main_large))
val notification = builder.build()
notification.flags = Notification.FLAG_NO_CLEAR
startForeground(NOTIFICATION_ID, notification)
val commonFunc = CommonFunc()
commonFunc.regContentObserver(applicationContext)
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
return START_STICKY
}
override fun onDestroy() {
super.onDestroy()
val commonFunc = CommonFunc()
commonFunc.remContentObserver(applicationContext)
}
Main Service自体は、Content Observerを登録すること以外何もしません。ただ、Main Serviceが動いていないのと、Content Observerが動かないので、Content Observerを動かすためだけにプカプカ浮いています。
3. アラーム
Content Observerは、アラームを起動します。
例えば、regContentObserver()内の
registerAlarmPhotoIn(context, delay)
です。
AlarmRecceiverは、RenameIntentService()を呼び出します。
class AlarmReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
val intentAutoRename = Intent(context, RenameIntentService::class.java)
val commonFunc = CommonFunc()
intentAutoRename.putExtra("Photo_Movie_Selection", intent?.getIntExtra("Photo_Movie_Selection", commonFunc.SelectionPhoto))
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context!!.startForegroundService(intentAutoRename);
} else {
context!!.startService(intentAutoRename);
}
}
}
4. RenameIntentService
RenameIntentService内では、
private fun renameProc(oldFile: File, newFile: File, internal_SD: Int) {
val prefs = getSharedPreferences("Setting", Context.MODE_PRIVATE)
lateinit var strTreeUri : String
when (internal_SD) {
INTERNAL_STORAGE -> strTreeUri = "treeUriIn"
SD_CARD -> strTreeUri = "treeUriEx"
}
if (prefs.getString(strTreeUri, null) != null) {
val treeUri = Uri.parse(prefs.getString(strTreeUri, null))
if (treeUri != null) {
if (!oldFile.equals(newFile)) {
val pickedDir = DocumentFile.fromTreeUri(applicationContext, treeUri)
if (pickedDir != null) {
if (oldFile.parent.contains(pickedDir.name as CharSequence)) {
for (documentFile in pickedDir.listFiles()) {
if (documentFile != null) {
if ((documentFile.name.equals(oldFile.name)) && !(documentFile.name.equals(newFile.name))) {
documentFile.renameTo(newFile.name)
scanMedia(applicationContext, oldFile, newFile)
}
}
}
}
}
}
}
}
}
という処理があり、リネームしています。
ここで、scanMedia()は、
private fun scanMedia(context: Context, oldFile: File, newFile: File) {
MediaScannerConnection.scanFile(context, arrayOf(oldFile.absolutePath, newFile.absolutePath), null, null)
}
となっており、旧ファイル名と新ファイル名をScanしています。ミソは、旧ファイル名もScanしている点。これをしないと、Nexsu5(Android 6)やNexus 6(Android 7)でGoogleフォトが正常に画像を認識してくれません。Googleフォトのバックアップが完了するとサムネイル画像がグレーアウトしてしまいます。
5. MainActivity.kt
treeUriは、
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
startActivityForResult(intent, REQUEST_INTERNAL_STORAGE_ACCESS)if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
startActivityForResult(intent, REQUEST_SD_CARD_ACCESS)
}public override fun onActivityResult(requestCode: Int, resultCode: Int, resultData: Intent?) {
if (resultCode == Activity.RESULT_OK) {
when (requestCode) {
REQUEST_INTERNAL_STORAGE_ACCESS -> {
// IntentからtreeのURIを取得します。
val treeUri = resultData!!.data
val pickedDir = DocumentFile.fromTreeUri(applicationContext, treeUri!!)
var isExistDirectory = false
for (documentFile in pickedDir!!.listFiles()) {
if (documentFile.isDirectory) isExistDirectory = true
}
if (isExistDirectory) {
Toast.makeText(applicationContext, R.string.select_LEAF_folder, Toast.LENGTH_SHORT).show()
val switch = findViewById<Switch>(R.id.switchInternal)
switch.isChecked = false
val prefs = getSharedPreferences("Setting", Context.MODE_PRIVATE)
val editor = prefs.edit()
editor.putBoolean("internal_storage_access", false)
editor.putString("treeUriIn",null)
editor.apply()
} else {
val prefs = getSharedPreferences("Setting", Context.MODE_PRIVATE)
val editor = prefs.edit()
editor.putString("treeUriIn", treeUri.toString())
editor.apply()
// 恒常的にPermissionを取得するにはContentResolverでtakePersistableUriPermissionを呼び出します。
applicationContext.getContentResolver().takePersistableUriPermission(
treeUri,
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
}
}
REQUEST_SD_CARD_ACCESS -> {
// IntentからtreeのURIを取得します。
val treeUri = resultData!!.data
val pickedDir = DocumentFile.fromTreeUri(applicationContext, treeUri!!)
var isExistDirectory = false
for (documentFile in pickedDir!!.listFiles()) {
if (documentFile.isDirectory) isExistDirectory = true
}
if (isExistDirectory) {
Toast.makeText(applicationContext, R.string.select_LEAF_folder, Toast.LENGTH_SHORT).show()
val switch = findViewById<Switch>(R.id.switchSD)
switch.isChecked = false
val prefs = getSharedPreferences("Setting", Context.MODE_PRIVATE)
val editor = prefs.edit()
editor.putBoolean("SDaccess", false)
editor.putString("treeUriEx",null)
editor.apply()
} else {
val prefs = getSharedPreferences("Setting", Context.MODE_PRIVATE)
val editor = prefs.edit()
editor.putString("treeUriEx", treeUri.toString())
editor.apply()
// 恒常的にPermissionを取得するにはContentResolverでtakePersistableUriPermissionを呼び出します。
applicationContext.getContentResolver().takePersistableUriPermission(
treeUri,
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
}
}
}
}
}
のように取得します。File.renameTo()を使わない理由は、SDカードのファイル名もリネームするためです。
Googleカレンダーのイベントを削除する
Googleカレンダーは便利なのですが、カレンダーのイベントを自動的にあるいは一括して削除する機能がないので、一括して削除する機能を作ってみました。
AndroidManifest.xml(アカウントにアクセスするパミッション)
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
<uses-permission android:name="android.permission.MANAGE_ACCOUNTS" />
<uses-permission android:name="android.permission.USE_CREDENTIALS" />
アカウントダイアログを出して、ユーザにGoogleアカウントを選択させる。
そのアカウントを認証する。
MainActivity.kt
private fun sendPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (ContextCompat.checkSelfPermission(this,
Manifest.permission.GET_ACCOUNTS) != PackageManager.PERMISSION_GRANTED) {
if (ActivityCompat.shouldShowRequestPermissionRationale(this,
Manifest.permission.GET_ACCOUNTS)) {
} else {
if (snackBar == null) {
val dataCenter = DataCenter()
snackBar = Snackbar.make(dataCenter.getListView()!!, R.string.Processing, Snackbar.LENGTH_INDEFINITE)
val snackView = snackBar!!.view as Snackbar.SnackbarLayout
val progressBar = ProgressBar(this, null, android.R.attr.progressBarStyleSmall)
snackView.addView(progressBar)
snackBar!!.show()
}
ActivityCompat.requestPermissions(this,
arrayOf(Manifest.permission.GET_ACCOUNTS),
REQUEST_PERMISSION_GET_ACCOUNTS)
}
} else {
if (snackBar == null) {
val dataCenter = DataCenter()
snackBar = Snackbar.make(dataCenter.getListView()!!, R.string.Processing, Snackbar.LENGTH_INDEFINITE)
val snackView = snackBar!!.view as Snackbar.SnackbarLayout
val progressBar = ProgressBar(this, null, android.R.attr.progressBarStyleSmall)
snackView.addView(progressBar)
snackBar!!.show()
}
// Googleアカウントの選択を表示する
// GoogleAccountCredentialのアカウント選択画面を使用する
mCredential = GoogleAccountCredential.usingOAuth2(this, Arrays.asList(SCOPES)).setBackOff(ExponentialBackOff())
startActivityForResult(
mCredential!!.newChooseAccountIntent(),
REQUEST_ACCOUNT_PICKER)
}
} else {
if (snackBar == null) {
val dataCenter = DataCenter()
snackBar = Snackbar.make(dataCenter.getListView()!!, R.string.Processing, Snackbar.LENGTH_INDEFINITE)
val snackView = snackBar!!.view as Snackbar.SnackbarLayout
val progressBar = ProgressBar(this, null, android.R.attr.progressBarStyleSmall)
snackView.addView(progressBar)
snackBar!!.show()
}
// Googleアカウントの選択を表示する
// GoogleAccountCredentialのアカウント選択画面を使用する
mCredential = GoogleAccountCredential.usingOAuth2(this, Arrays.asList(SCOPES)).setBackOff(ExponentialBackOff())
startActivityForResult(
mCredential!!.newChooseAccountIntent(),
REQUEST_ACCOUNT_PICKER)
}
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray
) {
if (snackBar == null) {
val dataCenter = DataCenter()
snackBar = Snackbar.make(dataCenter.getListView()!!, R.string.Processing, Snackbar.LENGTH_INDEFINITE)
val snackView = snackBar!!.view as Snackbar.SnackbarLayout
val progressBar = ProgressBar(this, null, android.R.attr.progressBarStyleSmall)
snackView.addView(progressBar)
snackBar!!.show()
}
// Googleアカウントの選択を表示する
// GoogleAccountCredentialのアカウント選択画面を使用する
mCredential = GoogleAccountCredential.usingOAuth2(this, Arrays.asList(SCOPES)).setBackOff(ExponentialBackOff())
startActivityForResult(
mCredential!!.newChooseAccountIntent(),
REQUEST_ACCOUNT_PICKER)
}
private fun authenticate(account : String?) {
val manager = AccountManager.get(this)
manager.getAuthToken(Account(account, "com.google"),
//"oauth2:https://www.googleapis.com/auth/calendar.events",
"oauth2:https://www.googleapis.com/auth/calendar",
null,
false,
AccountManagerCallback { accountManagerFuture ->
try {
val bundle = accountManagerFuture.result
val intent = bundle.get(AccountManager.KEY_INTENT) as Intent?
if (intent == null) {
val authToken = bundle.get(AccountManager.KEY_AUTHTOKEN)!!.toString()
val databaseManager = DatabaseManager()
databaseManager.replaceAccountDatabaseAuthToken(account!!, authToken)
if (snackBar!=null && snackBar!!.isShown) {
snackBar!!.dismiss()
snackBar = null
}
} else {
// 認証許可ダイアログを表示
startActivityForResult(intent, REQUEST_AUTH_DIALOG)
}
} catch (e: OperationCanceledException) {
e.printStackTrace()
} catch (e: IOException) {
e.printStackTrace()
} catch (e: AuthenticatorException) {
e.printStackTrace()
}
},
null)
}
override fun onActivityResult(
requestCode: Int,
resultCode: Int,
data: Intent?
) {
when (requestCode) {
REQUEST_ACCOUNT_PICKER -> {
if (resultCode == RESULT_OK && data != null && data.getExtras() != null) {
val accountName = data.getStringExtra(AccountManager.KEY_ACCOUNT_NAME)
pendingAccount = accountName
val databaseManager = DatabaseManager()
val cursor = databaseManager.queryAccountDatabase(accountName)
if (!cursor.moveToFirst()) {
databaseManager.insertAccountDatabase(accountName, databaseManager.DB_TRUE, DEFAULT_SHELF_LIFE, "")
}
updateList()
authenticate(accountName)
} else {
if (snackBar!=null && snackBar!!.isShown) {
snackBar!!.dismiss()
snackBar = null
}
}
}
REQUEST_AUTH_DIALOG -> {
if (resultCode == RESULT_OK) {
authenticate(pendingAccount)
pendingAccount = null
} else {
if (snackBar!=null && snackBar!!.isShown) {
snackBar!!.dismiss()
snackBar = null
}
}
}
REQUEST_AUTH_AGAIN -> {
if (resultCode == RESULT_OK) {
}
}
}
}
Googleカレンダーのイベント削除処理
MainActivity.kt
fun deleteCalendarEvent(context : Context, progressBar : ProgressBar) {
var mService : com.google.api.services.calendar.Calendar
val databaseManager = DatabaseManager()
val cursor = databaseManager.queryAccountDatabase()
if (cursor.moveToFirst()) {
do {
try {
val checked = cursor.getLong(cursor.getColumnIndex("checked"))
if (checked == databaseManager.DB_TRUE) {
val account = cursor.getString((cursor.getColumnIndex("account")))
mCredential = GoogleAccountCredential.usingOAuth2(context, Arrays.asList(SCOPES)).setBackOff(ExponentialBackOff())
mCredential!!.setSelectedAccountName(account)
val transport = AndroidHttp.newCompatibleTransport()
val jsonFactory = JacksonFactory.getDefaultInstance()
mService = com.google.api.services.calendar.Calendar.Builder(transport, jsonFactory, mCredential)
.build()
val shelfLife = cursor.getInt(cursor.getColumnIndex("shelf_life"))
val timeZone = TimeZone.getDefault()
val calendarToDelete = Calendar.getInstance(timeZone)
val year = calendarToDelete.get(Calendar.YEAR)
val month = calendarToDelete.get(Calendar.MONTH)
val date = calendarToDelete.get(Calendar.DATE)
val hour = calendarToDelete.get(Calendar.HOUR_OF_DAY)
val minute = calendarToDelete.get(Calendar.MINUTE)
val second = calendarToDelete.get(Calendar.SECOND)
val strYear = String.format("%04d", year)
val strMonth = String.format("%02d", month+1)
val strDate = String.format("%02d", date)
val strHour = String.format("%02d", hour)
val strMinute = String.format("%02d", minute)
val strSecond = String.format("%02d", second)
val maxDateTime1 = "$strYear-$strMonth-$strDate"
val maxDateTime2 = "$strHour:$strMinute:$strSecond"
val maxDateTime = DateTime.parseRfc3339(maxDateTime1 + "T" + maxDateTime2)
val events = mService.events().list(account)
.setTimeMax(maxDateTime)
.execute()
val eventItems = events.items
progressBar.max = eventItems.size
var i=1
for (event in eventItems) {
progressBar.progress = i
if (isDeletable(event, shelfLife, calendarToDelete, timeZone)) {
val eventId = event.id
mService.events().delete(account, eventId).execute()
}
i++
}
}
} catch (e: UserRecoverableAuthIOException) {
(context as Activity).startActivityForResult(e.intent, REQUEST_AUTH_AGAIN)
} catch (e: IOException) {
}
} while (cursor.moveToNext())
}
cursor.close()
}
private fun toRfc3339(calendar : Calendar) : String {
val year = calendar.get(Calendar.YEAR)
val month = calendar.get(Calendar.MONTH)
val date = calendar.get(Calendar.DATE)
val hour = calendar.get(Calendar.HOUR_OF_DAY)
val minute = calendar.get(Calendar.MINUTE)
val second = calendar.get(Calendar.SECOND)
val strYear = String.format("%04d", year)
val strMonth = String.format("%02d", month + 1)
val strDate = String.format("%02d", date)
val strHour = String.format("%02d", hour)
val strMinute = String.format("%02d", minute)
val strSecond = String.format("%02d", second)
val maxDateTime1 = "$strYear-$strMonth-$strDate"
val maxDateTime2 = "$strHour:$strMinute:$strSecond"
return DateTime.parseRfc3339(maxDateTime1 + "T" + maxDateTime2).toStringRfc3339()
}
private fun isDeletable(event : Event, shelfLife : Int, calendarNow: Calendar, timeZone: TimeZone) : Boolean {
val calendarToDelete = Calendar.getInstance(timeZone)
calendarToDelete.set(
calendarNow.get(Calendar.YEAR),
calendarNow.get(Calendar.MONTH),
calendarNow.get(Calendar.DATE),
calendarNow.get(Calendar.HOUR_OF_DAY),
calendarNow.get(Calendar.MINUTE),
calendarNow.get(Calendar.SECOND)
)
calendarToDelete.set(Calendar.MILLISECOND, 0)
calendarToDelete.add(Calendar.MONTH, -shelfLife)
if (event.endTimeUnspecified != null) {
if (event.endTimeUnspecified) return false
}
val calculateEndInRFC3339 = CalculateEndInRFC3339()
val strEventEnd : String? = calculateEndInRFC3339.calcEnd(event, timeZone)
if (strEventEnd == null) {
Log.d("GCDelete:strEventEnd","null")
return false
}
val strNow : String = toRfc3339(calendarToDelete)
return (strEventEnd.compareTo(strNow) < 0)
}
イベントの情報から、イベントの最終日を計算する処理
CalculateEndInRFC3339.kt
class CalculateEndInRFC3339 {
fun calcEnd(event : Event, timeZone: TimeZone) : String? {
var eventEnd : String? = null
if (event.recurrence == null) {
// 繰り返しではない
if (event.end != null) {
if (event.end.dateTime != null && event.end.date == null) {
// 終日ではない
eventEnd = event.end.dateTime.toStringRfc3339()
} else {
// 終日
eventEnd = event.end.date.toStringRfc3339()
}
} else {
}
} else {
// 繰り返しイベント
val calEventStart : Calendar = Calendar.getInstance(timeZone)
var strEventStart : String? = null
var eventStart : String? = null
if (event.start != null) {
if (event.start.dateTime != null && event.start.date == null) {
// 終日ではない
eventStart = event.start.dateTime.toStringRfc3339()
} else {
// 終日
eventStart = event.start.date.toStringRfc3339()
}
}
val pattern = Pattern.compile("[0-9]{4}-[0-9]{2}-[0-9]{2}")
val matcher = pattern.matcher(eventStart)
if (matcher.find()) {
strEventStart = matcher.group()
}
val locale = Locale.getDefault()
val sdf = SimpleDateFormat("yyyy-MM-dd",locale)
var date : Date? = null
try {
date = sdf.parse(strEventStart)
} catch (e: ParseException) {
return null
}
calEventStart.time = date
for (it in event.recurrence) {
if (isRRULE(it)) {
eventEnd = analyzeRRULE(it, calEventStart)
}
}
}
return eventEnd
}
private fun isRRULE(it : String) : Boolean {
val pattern = Pattern.compile("^RRULE")
val matcher = pattern.matcher(it)
return matcher.find()
}
private fun analyzeRRULE(it : String, calStart: Calendar) : String? {
var resultEndDate : String? = null
// UNTIL
val pattern01 = Pattern.compile("UNTIL=[0-9]{4}[0-9]{2}[0-9]{2}")
val matcher01 = pattern01.matcher(it)
if (matcher01.find()) {
val strBuffer = matcher01.group()
val pattern02 = Pattern.compile("[0-9]{4}[0-9]{2}[0-9]{2}")
val matcher02 = pattern02.matcher(strBuffer)
if (matcher02.find()) {
val strDate = matcher02.group()
val locale = Locale.getDefault()
val sdf = SimpleDateFormat("yyyyMMdd",locale)
var dateTemp : Date? = null
try {
dateTemp = sdf.parse(strDate)
} catch (e: ParseException) {
return null
}
val calEnd = Calendar.getInstance(calStart.timeZone)
calEnd.time = dateTemp
val year=String.format("%04d", calEnd.get(Calendar.YEAR))
val month=String.format("%02d", calEnd.get(Calendar.MONTH)+1)
val date=String.format("%02d", calEnd.get(Calendar.DATE))
resultEndDate = "$year-$month-$date"
}
}
// COUNT, INTERVAL
var count = 0
var countFlag = false
var interval = 0
var intervalFlag = false
var daily = false
var weekly = false
var monthly = false
var yearly= false
val pattern03 = Pattern.compile("COUNT=[0-9]*")
val matcher03 = pattern03.matcher(it)
if (matcher03.find()) {
val strBuffer = matcher03.group()
val pattern04 = Pattern.compile("[0-9]+")
val matcher04 = pattern04.matcher(strBuffer)
if (matcher04.find()) {
val strCount = matcher04.group()
count = strCount.toInt()
countFlag = true
}
}
val pattern05 = Pattern.compile("INTERVAL=[0-9]*")
val matcher05 = pattern05.matcher(it)
if (matcher05.find()) {
val strBuffer = matcher05.group()
val pattern06 = Pattern.compile("[0-9]+")
val matcher06 = pattern06.matcher(strBuffer)
if (matcher06.find()) {
val strInterval = matcher06.group()
interval = strInterval.toInt()
intervalFlag = true
}
}
val pattern07 = Pattern.compile("FREQ=DAILY")
val matcher07 = pattern07.matcher(it)
if (matcher07.find()) {
daily = true
}
val pattern08 = Pattern.compile("FREQ=WEEKLY")
val matcher08 = pattern08.matcher(it)
if (matcher08.find()) {
weekly = true
}
val pattern09 = Pattern.compile("FREQ=MONTHLY")
val matcher09 = pattern09.matcher(it)
if (matcher09.find()) {
monthly = true
}
val pattern10 = Pattern.compile("FREQ=YEARLY")
val matcher10 = pattern10.matcher(it)
if (matcher10.find()) {
yearly = true
}
val calEnd = calStart
if (countFlag && intervalFlag) {
if (daily) {
calEnd.add(Calendar.DATE, count*interval)
}
if (weekly) {
calEnd.add(Calendar.WEEK_OF_YEAR, count*interval)
}
if (monthly) {
calEnd.add(Calendar.MONTH, count*interval)
}
if (yearly) {
calEnd.add(Calendar.YEAR, count*interval)
}
val year=String.format("%04d", calEnd.get(Calendar.YEAR))
val month=String.format("%02d", calEnd.get(Calendar.MONTH)+1)
val date=String.format("%02d", calEnd.get(Calendar.DATE))
resultEndDate = "$year-$month-$date"
}
return resultEndDate
}
}
アプリをリリースするわけですが、その時につまづきました。
Google Play アプリ署名を有効にして、Android App Bundleとしてリリースしました。
この時に、API Consoleで、SHA1認証キーを入力するわけですが、
署名証明書のフィンガープリントには、Google Developer Consoleの「アプリのリリース」→「アプリの署名」の「アプリへの署名証明書」のSHA1フィンガープリントを持ってくる必要があります。keytoolで出力したフィンガープリントではうまくいきませんので、ご注意を!!
タッチに追従して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.build.gradle (app)
dependencies {
implementation 'com.google.api-client:google-api-client:1.25.0'
implementation 'com.google.oauth-client:google-oauth-client-jetty:1.23.0'
implementation 'com.google.apis:google-api-services-gmail:v1-rev83-1.23.0'
}
3.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" />
4.OAuth2 AuthTokenの取得
private fun authenticate() {
val manager = AccountManager.get(this@AuthenticationActivity)
val prefs3 = getSharedPreferences("setting3", MODE_PRIVATE)
val editor3 = prefs3.edit()
val previousAuthToken = prefs3.getString("AuthToken", null)
if (previousAuthToken != null) {
manager.invalidateAuthToken("com.google", previousAuthToken)
}
manager.getAuthToken(
Account(mailAddressFrom, "com.google"),
"oauth2:https://www.googleapis.com/auth/gmail.send",
null,
false,
{ future ->
try {
val bundle = future.result
val intent = bundle[AccountManager.KEY_INTENT] as Intent?
// 認証されていない場合は認証許可ダイアログを表示
if (intent != null) {
startActivityForResult.launch(intent)
} else {
val authToken = bundle.getString(AccountManager.KEY_AUTHTOKEN)
editor3.putString("AuthToken", authToken)
editor3.apply()
val prefs = getSharedPreferences("Startup", MODE_PRIVATE)
val editor = prefs.edit()
editor.putString("Authenticate", "authenticate_001")
editor.apply()
finish()
}
} catch (e: OperationCanceledException) {
e.printStackTrace()
} catch (e: IOException) {
e.printStackTrace()
} catch (e: AuthenticatorException) {
e.printStackTrace()
}
},
null
)
}
5.Gmailの送信ルーチン
public class GmailSendHelper {
public GmailSendHelper() {
}
public static MimeMessage createEmailWithAttachment(
InternetAddress[] internetAddressesTo,
String from,
String subject,
String textBody,
File file
) throws MessagingException {
Properties props = new Properties();
Session session = Session.getDefaultInstance(props, null);
MimeMessage email = new MimeMessage(session);
email.setFrom(new InternetAddress(from));
for (InternetAddress internetAddress : internetAddressesTo) {
email.addRecipient(javax.mail.Message.RecipientType.TO, internetAddress);
}
email.setSubject(subject);
MimeBodyPart mimeBodyPart = new MimeBodyPart();
mimeBodyPart.setText(textBody, "utf-8");
Multipart multipart = new MimeMultipart();
multipart.addBodyPart(mimeBodyPart);
mimeBodyPart = new MimeBodyPart();
FileDataSource source = new FileDataSource(file);
mimeBodyPart.setDataHandler(new DataHandler(source));
mimeBodyPart.setFileName(file.getName());
multipart.addBodyPart(mimeBodyPart);
email.setContent(multipart);
return email;
}
public static Message createMessageWithEmail(MimeMessage emailContent)
throws MessagingException, IOException {
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
emailContent.writeTo(buffer);
byte[] bytes = buffer.toByteArray();
String encodedEmail = Base64.encodeBase64URLSafeString(bytes);
Message message = new Message();
message.setRaw(encodedEmail);
return message;
}
public static Message sendMessage(
Gmail service,
String userId,
MimeMessage emailContent
) throws MessagingException, IOException {
Message message = createMessageWithEmail(emailContent);
message = service.users().messages().send(userId, message).execute();
return message;
}
}
6.Gmail送信ルーチンの呼び出し
続きを読むAccountManager manager = AccountManager.get(this);
manager.getAuthToken(new Account(mailAddressFrom, "com.google"),
"oauth2:https://www.googleapis.com/auth/gmail.send",
null,
false,
new AccountManagerCallback<Bundle>() {
@Override
public void run(AccountManagerFuture<Bundle> future) {
try {
bundle = future.getResult();
AsyncSendEmail asyncSendEmail = new AsyncSendEmail();
asyncSendEmail.execute();
} catch (OperationCanceledException | IOException | AuthenticatorException e) {
e.printStackTrace();
}
}
}, null);
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();
}