From 2b6d2f6c3aae28ede13ef4e2f60ded37b5f8e6a1 Mon Sep 17 00:00:00 2001 From: Mysochenko Yuriy Date: Tue, 13 Sep 2022 17:00:36 +0300 Subject: [PATCH] request proper permission to access files add icons for a shortcut --- app/build.gradle | 6 +- app/src/main/AndroidManifest.xml | 7 +- app/src/main/assets/licenses.html | 30 + .../shortcut/AddShortcutDialogActivity.kt | 170 +-- .../activityrunner/util/PermissionHelper.kt | 25 + .../sdex/commons/content/ContentManager.java | 996 ------------------ .../background_edit_icon_indicator.xml | 6 +- .../main/res/layout/activity_add_shortcut.xml | 14 +- app/src/main/res/menu/shortcut_icon.xml | 12 + app/src/main/res/values-uk/strings.xml | 4 + app/src/main/res/values/strings.xml | 4 + app/src/main/res/values/styles.xml | 2 +- app/src/main/res/values/themes.xml | 33 +- .../prod/play/release-notes/en-US/default.txt | 2 + 14 files changed, 220 insertions(+), 1091 deletions(-) create mode 100644 app/src/main/java/com/sdex/activityrunner/util/PermissionHelper.kt delete mode 100644 app/src/main/java/com/sdex/commons/content/ContentManager.java create mode 100644 app/src/main/res/menu/shortcut_icon.xml diff --git a/app/build.gradle b/app/build.gradle index c74b12d8..4d75bbb1 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -11,7 +11,7 @@ android { defaultConfig { applicationId "com.activitymanager" - minSdkVersion 16 + minSdkVersion 21 targetSdkVersion 33 versionCode 416 versionName "4.3.0" @@ -78,7 +78,7 @@ kapt { } dependencies { - implementation "androidx.activity:activity-ktx:1.5.1" + implementation "androidx.activity:activity-ktx:1.6.0-rc02" implementation "androidx.appcompat:appcompat:1.5.1" implementation "androidx.browser:browser:1.4.0" implementation "androidx.constraintlayout:constraintlayout:2.1.4" @@ -97,6 +97,8 @@ dependencies { implementation "com.google.dagger:hilt-android:$hilt_version" kapt "com.google.dagger:hilt-compiler:$hilt_version" implementation "com.jakewharton.timber:timber:5.0.1" + implementation "com.maltaisn:icondialog:3.3.0" + implementation "com.maltaisn:iconpack-community-material:5.3.45" implementation "com.simplecityapps:recyclerview-fastscroll:2.0.0" implementation "com.tomergoldst.android:tooltips:1.0.11" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f78c6d97..d9a2e17e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,7 +3,12 @@ xmlns:tools="http://schemas.android.com/tools" package="com.sdex.activityrunner"> - + + + + +
 
Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. +
+ + +
+    Copyright (c) 2014, Austin Andrews (http://materialdesignicons.com/),
+    with Reserved Font Name Material Design Icons.
+    Copyright (c) 2014, Google (http://www.google.com/design/)
+    uses the license at https://github.com/google/material-design-icons/blob/master/LICENSE
+
+    This Font Software is licensed under the SIL Open Font License, Version 1.1.
+    This license is copied below, and is also available with a FAQ at:
+    http://scripts.sil.org/OFL
+
+ diff --git a/app/src/main/java/com/sdex/activityrunner/shortcut/AddShortcutDialogActivity.kt b/app/src/main/java/com/sdex/activityrunner/shortcut/AddShortcutDialogActivity.kt index f8ec3cfe..7af48061 100644 --- a/app/src/main/java/com/sdex/activityrunner/shortcut/AddShortcutDialogActivity.kt +++ b/app/src/main/java/com/sdex/activityrunner/shortcut/AddShortcutDialogActivity.kt @@ -7,38 +7,59 @@ import android.graphics.Bitmap import android.graphics.drawable.Drawable import android.net.Uri import android.os.Bundle +import android.view.View +import android.widget.PopupMenu import android.widget.Toast +import androidx.activity.result.PickVisualMediaRequest +import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia +import androidx.activity.result.contract.ActivityResultContracts.RequestPermission import androidx.appcompat.app.AppCompatActivity -import androidx.core.content.ContextCompat import androidx.core.graphics.drawable.toBitmap -import androidx.core.view.isInvisible -import androidx.core.view.isVisible import com.bumptech.glide.request.RequestOptions -import com.bumptech.glide.request.target.SimpleTarget +import com.bumptech.glide.request.target.CustomTarget import com.bumptech.glide.request.transition.Transition +import com.maltaisn.icondialog.IconDialog +import com.maltaisn.icondialog.IconDialogSettings +import com.maltaisn.icondialog.data.Icon +import com.maltaisn.icondialog.pack.IconDrawableLoader +import com.maltaisn.icondialog.pack.IconPack +import com.maltaisn.icondialog.pack.IconPackLoader +import com.maltaisn.iconpack.mdi.createMaterialDesignIconPack import com.sdex.activityrunner.R import com.sdex.activityrunner.app.ActivityModel import com.sdex.activityrunner.databinding.ActivityAddShortcutBinding import com.sdex.activityrunner.db.history.HistoryModel import com.sdex.activityrunner.extensions.doAfterMeasure +import com.sdex.activityrunner.extensions.resolveColorAttr import com.sdex.activityrunner.extensions.serializable import com.sdex.activityrunner.glide.GlideApp import com.sdex.activityrunner.intent.converter.HistoryToLaunchParamsConverter import com.sdex.activityrunner.intent.converter.LaunchParamsToIntentConverter import com.sdex.activityrunner.preferences.TooltipPreferences import com.sdex.activityrunner.util.IntentUtils -import com.sdex.commons.content.ContentManager +import com.sdex.commons.content.getStoragePermission +import com.sdex.commons.content.isStoragePermissionGranted import com.tomergoldst.tooltips.ToolTip import com.tomergoldst.tooltips.ToolTipsManager -import timber.log.Timber -class AddShortcutDialogActivity : AppCompatActivity(), ContentManager.PickContentListener { +class AddShortcutDialogActivity : AppCompatActivity(), IconDialog.Callback { private lateinit var binding: ActivityAddShortcutBinding - private var contentManager: ContentManager? = null - private var bitmap: Bitmap? = null private val toolTipsManager = ToolTipsManager() + private val pickMedia = registerForActivityResult(PickVisualMedia()) { uri -> + if (uri != null) { + loadIcon(uri) + } + } + private val requestPermission = registerForActivityResult(RequestPermission()) { granted -> + if (granted) { + pickImage() + } + } + + private var bitmap: Bitmap? = null + private var iconPack: IconPack? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -56,7 +77,7 @@ class AddShortcutDialogActivity : AppCompatActivity(), ContentManager.PickConten .load(activityModel) .error(R.mipmap.ic_launcher) .apply(RequestOptions().centerCrop()) - .into(object : SimpleTarget() { + .into(object : CustomTarget() { override fun onResourceReady( resource: Drawable, transition: Transition? @@ -65,21 +86,30 @@ class AddShortcutDialogActivity : AppCompatActivity(), ContentManager.PickConten binding.icon.setImageDrawable(resource) showTooltip() } + + override fun onLoadCleared(placeholder: Drawable?) { + } }) + + val loader = IconPackLoader(applicationContext) + iconPack = createMaterialDesignIconPack(loader) + iconPack?.loadDrawables(loader.drawableLoader) } if (historyModel != null) { binding.icon.setImageResource(R.mipmap.ic_launcher) } - contentManager = ContentManager(this, this) - binding.icon.setOnClickListener { toolTipsManager.dismissAll() if (activityModel != null) { - contentManager?.pickContent(ContentManager.Content.IMAGE) + showIconMenu(it) } else { - Toast.makeText(this, R.string.error_intent_shortcut_icon, Toast.LENGTH_LONG).show() + Toast.makeText( + this, + R.string.error_intent_shortcut_icon, + Toast.LENGTH_LONG + ).show() } } @@ -119,14 +149,12 @@ class AddShortcutDialogActivity : AppCompatActivity(), ContentManager.PickConten binding.icon.doAfterMeasure { val builder = ToolTip.Builder( this@AddShortcutDialogActivity, - binding.icon, binding.content, - "Tap to change the icon", ToolTip.POSITION_BELOW - ) - builder.setBackgroundColor( - ContextCompat.getColor( - this@AddShortcutDialogActivity, R.color.blue_light - ) + binding.icon, + binding.content, + getString(R.string.shortcut_set_icon_tooltip), + ToolTip.POSITION_BELOW ) + builder.setBackgroundColor(resolveColorAttr(R.attr.colorTertiary)) builder.setTextAppearance(R.style.TooltipTextAppearance) toolTipsManager.show(builder.build()) preferences.showChangeIcon = false @@ -134,39 +162,60 @@ class AddShortcutDialogActivity : AppCompatActivity(), ContentManager.PickConten } } - private fun createHistoryModelShortcut(historyModel: HistoryModel, shortcutName: String) { - val historyToLaunchParamsConverter = HistoryToLaunchParamsConverter(historyModel) - val launchParams = historyToLaunchParamsConverter.convert() - val converter = LaunchParamsToIntentConverter(launchParams) - val intent = converter.convert() - IntentUtils.createLauncherIcon(this, shortcutName, intent, R.mipmap.ic_launcher) - } - - override fun onSaveInstanceState(outState: Bundle) { - super.onSaveInstanceState(outState) - contentManager?.onSaveInstanceState(outState) - } + override val iconDialogIconPack: IconPack? + get() = iconPack - override fun onRestoreInstanceState(savedInstanceState: Bundle) { - super.onRestoreInstanceState(savedInstanceState) - contentManager?.onRestoreInstanceState(savedInstanceState) + override fun onIconDialogIconsSelected(dialog: IconDialog, icons: List) { + val icon = icons.first() + if (icon.drawable == null) { + IconDrawableLoader(this).loadDrawable(icon) + } + val resource = icon.drawable + bitmap = resource?.toBitmap() + binding.icon.setImageDrawable(resource) } - override fun onRequestPermissionsResult( - requestCode: Int, permissions: Array, - grantResults: IntArray - ) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults) - contentManager?.onRequestPermissionsResult(requestCode, permissions, grantResults) + private fun showIconMenu(it: View?) { + val popupMenu = PopupMenu(this, it) + popupMenu.inflate(R.menu.shortcut_icon) + popupMenu.setOnMenuItemClickListener { menuItem -> + if (menuItem.itemId == R.id.pick_gallery) { + if (isStoragePermissionGranted(this)) { + pickImage() + } else { + requestPermission.launch(getStoragePermission()) + } + } else if (menuItem.itemId == R.id.pick_icon) { + if (iconPack != null) { + val iconDialog = IconDialog.newInstance(IconDialogSettings { + showSelectBtn = false + }) + iconDialog.show(supportFragmentManager, ICON_DIALOG_TAG) + } else { + Toast.makeText( + this, + R.string.icons_loading_error, + Toast.LENGTH_SHORT + ).show() + } + } + true + } + popupMenu.show() } - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - contentManager?.onActivityResult(requestCode, resultCode, data) + private fun pickImage() { + pickMedia.launch( + PickVisualMediaRequest(PickVisualMedia.ImageOnly) + ) } - override fun onContentLoaded(uri: Uri?, contentType: String?) { - uri?.let { loadIcon(uri) } + private fun createHistoryModelShortcut(historyModel: HistoryModel, shortcutName: String) { + val historyToLaunchParamsConverter = HistoryToLaunchParamsConverter(historyModel) + val launchParams = historyToLaunchParamsConverter.convert() + val converter = LaunchParamsToIntentConverter(launchParams) + val intent = converter.convert() + IntentUtils.createLauncherIcon(this, shortcutName, intent, R.mipmap.ic_launcher) } private fun loadIcon(uri: Uri) { @@ -177,39 +226,22 @@ class AddShortcutDialogActivity : AppCompatActivity(), ContentManager.PickConten .load(uri) .error(R.mipmap.ic_launcher) .apply(RequestOptions().centerCrop().override(size)) - .into(object : SimpleTarget(size, size) { + .into(object : CustomTarget(size, size) { override fun onResourceReady(resource: Bitmap, transition: Transition?) { bitmap = resource binding.icon.setImageBitmap(resource) - hideProgress() } - }) - } - - override fun onStartContentLoading() { - binding.progress.isVisible = true - binding.icon.isInvisible = true - } - - override fun onError(error: String?) { - Timber.e("Failed to load image: %s", error) - Toast.makeText(this, "Failed to load image", Toast.LENGTH_SHORT).show() - hideProgress() - } - - override fun onCanceled() { - hideProgress() - } - private fun hideProgress() { - binding.progress.isVisible = false - binding.icon.isVisible = true + override fun onLoadCleared(placeholder: Drawable?) { + } + }) } companion object { private const val ARG_ACTIVITY_MODEL = "arg_activity_model" private const val ARG_HISTORY_MODEL = "arg_history_model" + private const val ICON_DIALOG_TAG = "icons_dialog" fun start(context: Context, activityModel: ActivityModel) { context.startActivity( diff --git a/app/src/main/java/com/sdex/activityrunner/util/PermissionHelper.kt b/app/src/main/java/com/sdex/activityrunner/util/PermissionHelper.kt new file mode 100644 index 00000000..1c186de8 --- /dev/null +++ b/app/src/main/java/com/sdex/activityrunner/util/PermissionHelper.kt @@ -0,0 +1,25 @@ +@file:JvmName("PermissionHelper") + +package com.sdex.commons.content + +import android.Manifest.permission.READ_EXTERNAL_STORAGE +import android.Manifest.permission.READ_MEDIA_IMAGES +import android.Manifest.permission.WRITE_EXTERNAL_STORAGE +import android.content.Context +import android.content.pm.PackageManager +import android.os.Build +import androidx.core.content.ContextCompat + +fun getStoragePermission(): String = when { + Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> READ_MEDIA_IMAGES + Build.VERSION.SDK_INT > Build.VERSION_CODES.Q -> READ_EXTERNAL_STORAGE + else -> WRITE_EXTERNAL_STORAGE +} + +fun isStoragePermissionGranted(context: Context): Boolean { + return hasPermission(context, getStoragePermission()) +} + +private fun hasPermission(context: Context, permission: String): Boolean = + ContextCompat.checkSelfPermission(context, permission) == + PackageManager.PERMISSION_GRANTED diff --git a/app/src/main/java/com/sdex/commons/content/ContentManager.java b/app/src/main/java/com/sdex/commons/content/ContentManager.java deleted file mode 100644 index b1d6a0a3..00000000 --- a/app/src/main/java/com/sdex/commons/content/ContentManager.java +++ /dev/null @@ -1,996 +0,0 @@ -/******************************************************************************* - * Copyright 2016 Anton Bevza stfalcon.com - * https://github.com/stfalcon-studio/ContentManager - *

- * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

- * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - *******************************************************************************/ -package com.sdex.commons.content; - -import android.annotation.TargetApi; -import android.app.Activity; -import android.content.ActivityNotFoundException; -import android.content.ClipData; -import android.content.ContentResolver; -import android.content.ContentUris; -import android.content.ContentValues; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.database.Cursor; -import android.graphics.Bitmap; -import android.graphics.Matrix; -import android.media.ExifInterface; -import android.net.Uri; -import android.os.Build; -import android.os.Bundle; -import android.os.Environment; -import android.os.Handler; -import android.os.ParcelFileDescriptor; -import android.provider.DocumentsContract; -import android.provider.MediaStore; -import android.util.Log; -import android.webkit.MimeTypeMap; - -import androidx.core.app.ActivityCompat; -import androidx.core.content.ContextCompat; -import androidx.fragment.app.Fragment; - -import java.io.BufferedInputStream; -import java.io.BufferedOutputStream; -import java.io.File; -import java.io.FileDescriptor; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.nio.channels.FileChannel; -import java.text.DateFormat; -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; -import java.util.Locale; -import java.util.TimeZone; -import java.util.UUID; - -public class ContentManager { - - private final static int PERMISSION_REQUEST_CODE = 333; - - /** - * For save and restore instance state - */ - private static final String DATE_CAMERA_INTENT_STARTED_STATE = - "ContentManager.DATE_CAMERA_INTENT_STARTED_STATE"; - private static final String CAMERA_PIC_URI_STATE = "ContentManager.CAMERA_PIC_URI_STATE"; - private static final String PHOTO_URI_STATE = "ContentManager.PHOTO_URI_STATE"; - private static final String ROTATE_X_DEGREES_STATE = "ContentManager.ROTATE_X_DEGREES_STATE"; - private static final String SAVED_TASK_STATE = "ContentManager.SAVED_TASK"; - private static final String TARGET_FILE_STATE = "ContentManager.TARGET_FILE"; - private static final String SAVED_CONTENT_STATE = "ContentManager.SAVED_CONTENT"; - - /** - * Request codes - */ - private static final int CONTENT_PICKER = 15; // request codes - private static final int CONTENT_TAKE = 16; // - - /** - * Date and time the camera intent was started. - */ - private Date dateCameraIntentStarted = null; - /** - * Default location where we want the photo to be ideally stored. - */ - private Uri preDefinedCameraUri = null; - - private Uri photoUri = null; - /** - * Potential 3rd location of photo data. - */ - private Uri photoUriIn3rdLocation = null; - /** - * Orientation of the retrieved photo. - */ - private int rotateXDegrees = 0; - /** - * Result target file - */ - private File targetFile; - /** - * Result callback - */ - private PickContentListener pickContentListener; - /** - * For monitor the load process - */ - private Handler handler; - private int progressPercent = 0; - /** - * Activity, fragment - */ - private Activity activity; - private Fragment fragment; - - private int savedTask; - private Content savedContent; - - public ContentManager(Activity activity, PickContentListener pickContentListener) { - this.activity = activity; - this.pickContentListener = pickContentListener; - handler = new Handler(); - } - - public ContentManager(Activity activity, PickContentListener pickContentListener, - Fragment fragment) { - this(activity, pickContentListener); - this.fragment = fragment; - } - - /** - * Need to call in onSaveInstanceState method of activity - */ - public void onSaveInstanceState(Bundle savedInstanceState) { - if (dateCameraIntentStarted != null) { - savedInstanceState - .putString(DATE_CAMERA_INTENT_STARTED_STATE, dateToString(dateCameraIntentStarted)); - } - if (preDefinedCameraUri != null) { - savedInstanceState.putString(CAMERA_PIC_URI_STATE, preDefinedCameraUri.toString()); - } - if (photoUri != null) { - savedInstanceState.putString(PHOTO_URI_STATE, photoUri.toString()); - } - if (targetFile != null) { - savedInstanceState.putSerializable(TARGET_FILE_STATE, targetFile); - } - if (savedContent != null) { - savedInstanceState.putSerializable(SAVED_CONTENT_STATE, savedContent); - } - savedInstanceState.putInt(SAVED_TASK_STATE, savedTask); - savedInstanceState.putInt(ROTATE_X_DEGREES_STATE, rotateXDegrees); - } - - /** - * Call to reinitialize the helpers instance state. - * Need to call in onRestoreInstanceState method of activity - */ - public void onRestoreInstanceState(Bundle savedInstanceState) { - if (savedInstanceState != null) { - if (savedInstanceState.containsKey(DATE_CAMERA_INTENT_STARTED_STATE)) { - dateCameraIntentStarted = stringToDate( - savedInstanceState.getString(DATE_CAMERA_INTENT_STARTED_STATE)); - } - if (savedInstanceState.containsKey(CAMERA_PIC_URI_STATE)) { - preDefinedCameraUri = Uri.parse(savedInstanceState.getString(CAMERA_PIC_URI_STATE)); - } - if (savedInstanceState.containsKey(PHOTO_URI_STATE)) { - photoUri = Uri.parse(savedInstanceState.getString(PHOTO_URI_STATE)); - } - if (savedInstanceState.containsKey(ROTATE_X_DEGREES_STATE)) { - rotateXDegrees = savedInstanceState.getInt(ROTATE_X_DEGREES_STATE); - } - if (savedInstanceState.containsKey(TARGET_FILE_STATE)) { - targetFile = (File) savedInstanceState.getSerializable(TARGET_FILE_STATE); - } - if (savedInstanceState.containsKey(SAVED_CONTENT_STATE)) { - savedContent = (Content) savedInstanceState.getSerializable(SAVED_CONTENT_STATE); - } - if (savedInstanceState.containsKey(SAVED_TASK_STATE)) { - savedTask = savedInstanceState.getInt(SAVED_TASK_STATE); - } - } - } - - /** - * Need to call in onActivityResult method of activity or fragment - */ - public void onActivityResult(int requestCode, int resultCode, Intent data) { - if (requestCode == CONTENT_PICKER) { - if (resultCode == Activity.RESULT_OK) { - handleContentData(data); - } else { - pickContentListener.onCanceled(); - } - } - if (requestCode == CONTENT_TAKE) { - onCameraIntentResult(requestCode, resultCode, data); - } - } - - /** - * Pick image or video content from storage or google acc - * - * @param content image or video - */ - public void pickContent(Content content) { - savedTask = CONTENT_PICKER; - savedContent = content; - if (isStoragePermissionGranted(activity, fragment)) { - this.targetFile = createFile(content); - if (Build.VERSION.SDK_INT < 19) { - Intent photoPickerIntent = new Intent(Intent.ACTION_PICK); - photoPickerIntent.setType(content.toString()); - if (fragment == null) { - activity.startActivityForResult(photoPickerIntent, CONTENT_PICKER); - } else { - fragment.startActivityForResult(photoPickerIntent, CONTENT_PICKER); - } - } else { - Intent photoPickerIntent = new Intent(Intent.ACTION_GET_CONTENT); - photoPickerIntent.setType(content.toString()); - photoPickerIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - photoPickerIntent.addCategory(Intent.CATEGORY_OPENABLE); - if (photoPickerIntent.resolveActivity(activity.getPackageManager()) != null) { - if (fragment == null) { - activity.startActivityForResult(photoPickerIntent, CONTENT_PICKER); - } else { - fragment.startActivityForResult(photoPickerIntent, CONTENT_PICKER); - } - } - } - } - } - - public void takePhoto() { - savedTask = CONTENT_TAKE; - if (isStoragePermissionGranted(activity, fragment)) { - if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { - try { - boolean setPreDefinedCameraUri = isSetPreDefinedCameraUri(); - - dateCameraIntentStarted = new Date(); - Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); - if (setPreDefinedCameraUri) { - String filename = System.currentTimeMillis() + ".jpg"; - ContentValues values = new ContentValues(); - values.put(MediaStore.Images.Media.TITLE, filename); - - preDefinedCameraUri = activity.getContentResolver().insert( - MediaStore.Images.Media.EXTERNAL_CONTENT_URI, - values); - intent.putExtra(MediaStore.EXTRA_OUTPUT, preDefinedCameraUri); - } - if (fragment == null) { - activity.startActivityForResult(intent, CONTENT_TAKE); - } else { - fragment.startActivityForResult(intent, CONTENT_TAKE); - } - } catch (ActivityNotFoundException e) { - pickContentListener.onError(""); - } - } else { - pickContentListener.onError(""); - } - } - } - - - /** - * Check device model and return is need to set predefined camera uri - */ - private boolean isSetPreDefinedCameraUri() { - boolean setPreDefinedCameraUri = false; - - // NOTE: Do NOT SET: intent.putExtra(MediaStore.EXTRA_OUTPUT, cameraPicUri) - // on Samsung Galaxy S2/S3/.. for the following reasons: - // 1.) it will break the correct picture orientation - // 2.) the photo will be stored in two locations (the given path and, additionally, in the MediaStore) - String manufacturer = Build.MANUFACTURER.toLowerCase(Locale.ENGLISH); - String model = Build.MODEL.toLowerCase(Locale.ENGLISH); - String buildType = Build.TYPE.toLowerCase(Locale.ENGLISH); - String buildDevice = Build.DEVICE.toLowerCase(Locale.ENGLISH); - String buildId = Build.ID.toLowerCase(Locale.ENGLISH); -// String sdkVersion = android.os.Build.VERSION.RELEASE.toLowerCase(Locale.ENGLISH); - - if (!(manufacturer.contains("samsung")) && !(manufacturer.contains("sony"))) { - setPreDefinedCameraUri = true; - } - if (manufacturer.contains("samsung") && model.contains("galaxy nexus")) { //TESTED - setPreDefinedCameraUri = true; - } - if (manufacturer.contains("samsung") && model.contains("gt-n7000") && buildId - .contains("imm76l")) { //TESTED - setPreDefinedCameraUri = true; - } - - if (buildType.contains("userdebug") && buildDevice.contains("ariesve")) { //TESTED - setPreDefinedCameraUri = true; - } - if (buildType.contains("userdebug") && buildDevice.contains("crespo")) { //TESTED - setPreDefinedCameraUri = true; - } - if (buildType.contains("userdebug") && buildDevice.contains("gt-i9100")) { //TESTED - setPreDefinedCameraUri = true; - } - - /////////////////////////////////////////////////////////////////////////// - // TEST - if (manufacturer.contains("samsung") && model - .contains("sgh-t999l")) { //T-Mobile LTE enabled Samsung S3 - setPreDefinedCameraUri = true; - } - if (buildDevice.contains("cooper")) { - setPreDefinedCameraUri = true; - } - if (buildType.contains("userdebug") && buildDevice.contains("t0lte")) { - setPreDefinedCameraUri = true; - } - if (buildType.contains("userdebug") && buildDevice.contains("kot49h")) { - setPreDefinedCameraUri = true; - } - if (buildType.contains("userdebug") && buildDevice.contains("t03g")) { - setPreDefinedCameraUri = true; - } - if (buildType.contains("userdebug") && buildDevice.contains("gt-i9300")) { - setPreDefinedCameraUri = true; - } - if (buildType.contains("userdebug") && buildDevice.contains("gt-i9195")) { - setPreDefinedCameraUri = true; - } - if (buildType.contains("userdebug") && buildDevice.contains("xperia u")) { - setPreDefinedCameraUri = true; - } - - /////////////////////////////////////////////////////////////////////////// - return setPreDefinedCameraUri; - } - - /** - * Process result of camera intent - */ - private void onCameraIntentResult(int requestCode, int resultCode, Intent intent) { - if (resultCode == Activity.RESULT_OK) { - Cursor myCursor = null; - Date dateOfPicture = null; - try { - // Create a Cursor to obtain the file Path for the large image - String[] largeFileProjection = {MediaStore.Images.ImageColumns._ID, - MediaStore.Images.ImageColumns.DATA, - MediaStore.Images.ImageColumns.ORIENTATION, - MediaStore.Images.ImageColumns.DATE_TAKEN}; - String largeFileSort = MediaStore.Images.ImageColumns._ID + " DESC"; - myCursor = activity.getContentResolver().query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, - largeFileProjection, - null, null, - largeFileSort); - myCursor.moveToFirst(); - if (!myCursor.isAfterLast()) { - // This will actually give you the file path location of the image. - String largeImagePath = myCursor.getString(myCursor - .getColumnIndexOrThrow(MediaStore.Images.ImageColumns.DATA)); - photoUri = Uri.fromFile(new File(largeImagePath)); - if (photoUri != null) { - dateOfPicture = new Date(myCursor.getLong( - myCursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.DATE_TAKEN))); - if (dateOfPicture != null && dateOfPicture.after(dateCameraIntentStarted)) { - rotateXDegrees = myCursor.getInt(myCursor - .getColumnIndexOrThrow(MediaStore.Images.ImageColumns.ORIENTATION)); - } else { - photoUri = null; - } - } - if (myCursor.moveToNext() && !myCursor.isAfterLast()) { - String largeImagePath3rdLocation = myCursor.getString(myCursor - .getColumnIndexOrThrow(MediaStore.Images.ImageColumns.DATA)); - Date dateOfPicture3rdLocation = new Date(myCursor.getLong( - myCursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.DATE_TAKEN))); - if (dateOfPicture3rdLocation != null && dateOfPicture3rdLocation - .after(dateCameraIntentStarted)) { - photoUriIn3rdLocation = Uri.fromFile(new File(largeImagePath3rdLocation)); - } - } - } - } catch (Exception e) { - } finally { - if (myCursor != null && !myCursor.isClosed()) { - myCursor.close(); - } - } - - if (photoUri == null) { - try { - photoUri = intent.getData(); - } catch (Exception e) { - } - } - - if (photoUri == null) { - photoUri = preDefinedCameraUri; - } - - try { - if (photoUri != null && new File(photoUri.getPath()).length() <= 0) { - if (preDefinedCameraUri != null) { - Uri tempUri = photoUri; - photoUri = preDefinedCameraUri; - preDefinedCameraUri = tempUri; - } - } - } catch (Exception e) { - } - - photoUri = getFileUriFromContentUri(photoUri); - preDefinedCameraUri = getFileUriFromContentUri(preDefinedCameraUri); - try { - if (photoUriIn3rdLocation != null) { - if (photoUriIn3rdLocation.equals(photoUri) || photoUriIn3rdLocation - .equals(preDefinedCameraUri)) { - photoUriIn3rdLocation = null; - } else { - photoUriIn3rdLocation = getFileUriFromContentUri(photoUriIn3rdLocation); - } - } - } catch (Exception e) { - } - - if (photoUri != null) { - pickContentListener.onContentLoaded(photoUri, Content.IMAGE.toString()); - } else { - pickContentListener.onError(""); - } - } else if (resultCode == Activity.RESULT_CANCELED) { - pickContentListener.onCanceled(); - } else { - pickContentListener.onCanceled(); - } - } - - /** - * Async load content data - * - * @param data result intent - */ - private void handleContentData(final Intent data) { - if (data != null) { - if (savedContent != Content.FILE) { - handleMediaContent(data); - } else { - handleFileContent(data); - } - } else { - handler.post(new Runnable() { - @Override - public void run() { - pickContentListener.onError("Data null"); - } - }); - } - } - - private void handleMediaContent(final Intent data) { - pickContentListener.onStartContentLoading(); - - new Thread(new Runnable() { - public void run() { - try { - Uri contentVideoUri = data.getData(); - FileInputStream in = (FileInputStream) activity.getContentResolver() - .openInputStream(contentVideoUri); - if (targetFile == null) { - targetFile = createFile(savedContent); - } - FileOutputStream out = new FileOutputStream(targetFile); - FileChannel inChannel = in.getChannel(); - FileChannel outChannel = out.getChannel(); - inChannel.transferTo(0, inChannel.size(), outChannel); - - in.close(); - out.close(); - - handler.post(new Runnable() { - @Override - public void run() { - pickContentListener - .onContentLoaded(Uri.fromFile(targetFile), savedContent.toString()); - } - }); - } catch (final Exception e) { - handler.post(new Runnable() { - @Override - public void run() { - pickContentListener.onError(e.getMessage()); - } - }); - } - } - }).start(); - } - - private void handleFileContent(final Intent intent) { - List uris = new ArrayList<>(); - if (intent.getDataString() != null) { - String uri = intent.getDataString(); - uris.add(uri); - } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { - if (intent.getClipData() != null) { - ClipData clipData = intent.getClipData(); - for (int i = 0; i < clipData.getItemCount(); i++) { - ClipData.Item item = clipData.getItemAt(i); - Log.d("TAG", "Item [" + i + "]: " + item.getUri().toString()); - uris.add(item.getUri().toString()); - } - } - } - if (intent.hasExtra("uris")) { - ArrayList paths = intent.getParcelableArrayListExtra("uris"); - for (int i = 0; i < paths.size(); i++) { - uris.add(paths.get(i).toString()); - } - } - - //TODO Handle multiple file choose - processFile(uris.get(0)); - } - - private void processFile(final String queryUri) { - pickContentListener.onStartContentLoading(); - - new Thread(new Runnable() { - @Override - public void run() { - String originalPath = null; - String uri = queryUri; - if (uri.startsWith("file://") || uri.startsWith("/")) { - originalPath = sanitizeUri(uri); - } else if (uri.startsWith("content:")) { - originalPath = getAbsolutePathIfAvailable(uri); - } - uri = originalPath; - // Still content:: Try ContentProvider stream import - if (uri.startsWith("content:")) { - originalPath = getFileFromContentProvider(originalPath); - } - - // Check for URL Encoded file paths - try { - String decodedURL = Uri.parse(Uri.decode(originalPath)).toString(); - if (!decodedURL.equals(originalPath)) { - originalPath = decodedURL; - } - } catch (Exception e) { - e.printStackTrace(); - } - - final String finalOriginalPath = originalPath; - handler.post(new Runnable() { - @Override - public void run() { - pickContentListener - .onContentLoaded(Uri.parse(finalOriginalPath), savedContent.toString()); - } - }); - } - }).start(); - - } - - // Try to get a local copy if available - - private String getAbsolutePathIfAvailable(String uri) { - String[] projection = {MediaStore.MediaColumns.DATA, MediaStore.MediaColumns.DISPLAY_NAME, - MediaStore.MediaColumns.MIME_TYPE}; - String originalPath; - if (uri.startsWith( - "content://com.android.gallery3d.provider")) { - originalPath = (Uri.parse(uri.replace( - "com.android.gallery3d", "com.google.android.gallery3d")).toString()); - } else { - originalPath = uri; - } - // Try to see if there's a cached local copy that is available - if (uri.startsWith("content://")) { - try { - Cursor cursor = activity.getContentResolver().query(Uri.parse(uri), projection, - null, null, null); - cursor.moveToFirst(); - try { - // Samsung Bug - if (!uri.contains("com.sec.android.gallery3d.provider")) { - String path = cursor.getString(cursor - .getColumnIndexOrThrow(MediaStore.MediaColumns.DATA)); - if (path != null) { - originalPath = path; - } - } - } catch (Exception e) { - e.printStackTrace(); - } - cursor.close(); - } catch (Exception e) { - e.printStackTrace(); - } - } - - // Check if DownloadsDocument in which case, we can get the local copy by using the content provider - if (originalPath.startsWith("content:") && isFileDownloadsDocument(Uri.parse(originalPath))) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - originalPath = getFilePath(originalPath); - } - } - - return originalPath; - } - - - // If starts with file: (For some content providers, remove the file prefix) - private String sanitizeUri(String uri) { - if (uri.startsWith("file://")) { - return uri.substring(7); - } - return uri; - } - - /** - * Create image file in directory of pictures - */ - public static File createFile(Content content) { - // Create an image file name - String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date()); - String type = content.equals(Content.IMAGE) ? ".jpg" : ".mp4"; - String imageFileName = "IMAGE_" + timeStamp + "_"; - File storageDir = Environment.getExternalStoragePublicDirectory( - Environment.DIRECTORY_PICTURES); - File image = null; - try { - image = File.createTempFile( - imageFileName, /* prefix */ - type, /* suffix */ - storageDir /* directory */ - ); - } catch (IOException e) { - e.printStackTrace(); - } - - return image; - } - - private Uri getFileUriFromContentUri(Uri cameraPicUri) { - Cursor cursor = null; - try { - if (cameraPicUri != null - && cameraPicUri.toString().startsWith("content")) { - String[] proj = {MediaStore.Images.Media.DATA}; - cursor = activity.getContentResolver().query(cameraPicUri, proj, null, null, null); - cursor.moveToFirst(); - // This will actually give you the file path location of the image. - String largeImagePath = cursor.getString(cursor - .getColumnIndexOrThrow(MediaStore.Images.ImageColumns.DATA)); - return Uri.fromFile(new File(largeImagePath)); - } - return cameraPicUri; - } catch (Exception e) { - return cameraPicUri; - } finally { - if (cursor != null && !cursor.isClosed()) { - cursor.close(); - } - } - } - - /** - * Result callback - */ - public interface PickContentListener { - - void onContentLoaded(Uri uri, String contentType); - - void onStartContentLoading(); - - void onError(String error); - - void onCanceled(); - } - - /** - * Content type - */ - public enum Content { - VIDEO("video/*"), - IMAGE("image/*"), - FILE("*/*"); - - private final String text; - - private Content(final String text) { - this.text = text; - } - - @Override - public String toString() { - return text; - } - } - - /** - * File name date format - */ - public final static String dateFormat = "yyyy-MM-dd HH:mm:ss.SSSZ"; - - public final static TimeZone utc = TimeZone.getTimeZone("UTC"); - - /** - * Converts a Date object to a string representation. - * - * @return date as String - */ - public static String dateToString(Date date) { - if (date == null) { - return null; - } else { - DateFormat df = new SimpleDateFormat(dateFormat); - df.setTimeZone(utc); - return df.format(date); - } - } - - /** - * Converts a string representation of a date to its respective Date object. - * - * @return Date - */ - public static Date stringToDate(String dateAsString) { - try { - DateFormat df = new SimpleDateFormat(dateFormat); - df.setTimeZone(utc); - return df.parse(dateAsString); - } catch (ParseException e) { - return null; - } catch (NullPointerException e) { - return null; - } - } - - - /** - * Some devices return wrong rotated image so we can fix it by this method - */ - public static void fixImageRatation(Uri uri, Bitmap realImage) { - File pictureFile = new File(uri.getPath()); - - try { - FileOutputStream fos = new FileOutputStream(pictureFile); - ExifInterface exif = new ExifInterface(pictureFile.toString()); - - if (exif.getAttribute(ExifInterface.TAG_ORIENTATION).equalsIgnoreCase("6")) { - realImage = rotate(realImage, 90); - } else if (exif.getAttribute(ExifInterface.TAG_ORIENTATION).equalsIgnoreCase("8")) { - realImage = rotate(realImage, 270); - } else if (exif.getAttribute(ExifInterface.TAG_ORIENTATION).equalsIgnoreCase("3")) { - realImage = rotate(realImage, 180); - } - - boolean bo = realImage.compress(Bitmap.CompressFormat.JPEG, 100, fos); - - fos.close(); - - Log.d("Info", bo + ""); - - } catch (FileNotFoundException e) { - Log.d("Info", "File not found: " + e.getMessage()); - } catch (IOException e) { - Log.d("TAG", "Error accessing file: " + e.getMessage()); - } - } - - public static Bitmap rotate(Bitmap bitmap, int degree) { - int w = bitmap.getWidth(); - int h = bitmap.getHeight(); - - Matrix mtx = new Matrix(); - mtx.postRotate(degree); - - return Bitmap.createBitmap(bitmap, 0, 0, w, h, mtx, true); - } - - public void onRequestPermissionsResult(int requestCode, String[] permissions, - int[] grantResults) { - if (requestCode == PERMISSION_REQUEST_CODE) { - if (grantResults.length > 0) { - if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { - switch (savedTask) { - case CONTENT_PICKER: - pickContent(savedContent); - break; - case CONTENT_TAKE: - takePhoto(); - break; - } - } - } - } - } - - //For fragments - public static boolean isStoragePermissionGranted(Activity activity, Fragment fragment) { - if (Build.VERSION.SDK_INT >= 23) { - if (ContextCompat - .checkSelfPermission(activity, android.Manifest.permission.WRITE_EXTERNAL_STORAGE) - == PackageManager.PERMISSION_GRANTED) { - return true; - } else { - if (fragment == null) { - ActivityCompat.requestPermissions(activity, - new String[]{android.Manifest.permission.WRITE_EXTERNAL_STORAGE}, - PERMISSION_REQUEST_CODE); - } else { - fragment.requestPermissions( - new String[]{android.Manifest.permission.WRITE_EXTERNAL_STORAGE}, - PERMISSION_REQUEST_CODE); - } - return false; - } - } else { - return true; - } - } - - - @TargetApi(Build.VERSION_CODES.KITKAT) - private String getFilePath(String originalPath) { - - final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; - Uri uri = Uri.parse(originalPath); - // DocumentProvider - if (isKitKat && DocumentsContract.isDocumentUri(activity, uri)) { - // ExternalStorageProvider - if (isFileDownloadsDocument(uri)) { - final String id = DocumentsContract.getDocumentId(uri); - final Uri contentUri = ContentUris.withAppendedId( - Uri.parse("content://downloads/public_downloads"), Long.valueOf(id)); - - return getFileData(contentUri, null, null); - } - // MediaProvider - else if (isFileMediaDocument(uri)) { - final String docId = DocumentsContract.getDocumentId(uri); - final String[] split = docId.split(":"); - final String type = split[0]; - - Uri contentUri = null; - if ("image".equals(type)) { - contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; - } else if ("video".equals(type)) { - contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; - } else if ("audio".equals(type)) { - contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; - } - - final String selection = "_id=?"; - final String[] selectionArgs = new String[]{ - split[1] - }; - - return getFileData(contentUri, selection, selectionArgs); - } - } - // MediaStore (and general) - else if ("content".equalsIgnoreCase(uri.getScheme())) { - return getFileData(uri, null, null); - } - // File - else if ("file".equalsIgnoreCase(uri.getScheme())) { - return uri.getPath(); - } - - return null; - } - - - private String getFileData(Uri uri, String selection, - String[] selectionArgs) { - Cursor cursor = null; - String[] projection = {MediaStore.MediaColumns.DATA}; - - try { - cursor = activity.getContentResolver().query(uri, projection, selection, selectionArgs, - null); - if (cursor != null && cursor.moveToFirst()) { - return cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATA)); - } - } finally { - if (cursor != null) { - cursor.close(); - } - } - return null; - } - - private boolean isFileDownloadsDocument(Uri uri) { - return "com.android.providers.downloads.documents".equals(uri.getAuthority()); - } - - private boolean isFileMediaDocument(Uri uri) { - return "com.android.providers.media.documents".equals(uri.getAuthority()); - } - - protected String getFileFromContentProvider(String uri) { - - BufferedInputStream inputStream = null; - BufferedOutputStream outStream = null; - ParcelFileDescriptor parcelFileDescriptor = null; - try { - String localFilePath = generateFileName(uri); - parcelFileDescriptor = activity - .getContentResolver().openFileDescriptor(Uri.parse(uri), "r"); - - FileDescriptor fileDescriptor = parcelFileDescriptor - .getFileDescriptor(); - - inputStream = new BufferedInputStream(new FileInputStream(fileDescriptor)); - BufferedInputStream reader = new BufferedInputStream(inputStream); - - outStream = new BufferedOutputStream( - new FileOutputStream(localFilePath)); - byte[] buf = new byte[2048]; - int len; - while ((len = reader.read(buf)) > 0) { - outStream.write(buf, 0, len); - } - outStream.flush(); - uri = localFilePath; - } catch (IOException e) { - return uri; - } finally { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { - try { - parcelFileDescriptor.close(); - } catch (IOException e) { - e.printStackTrace(); - } - } - try { - outStream.flush(); - outStream.close(); - inputStream.close(); - } catch (IOException e) { - e.printStackTrace(); - } - } - return uri; - } - - private String generateFileName(String file) { - String fileName = UUID.randomUUID().toString() + "." + guessFileExtensionFromUrl(file); - String probableFileName = fileName; - File directory = activity.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS); - File probableFile = new File(directory.getAbsolutePath() + File.separator + probableFileName); - int counter = 0; - while (probableFile.exists()) { - counter++; - if (fileName.contains(".")) { - int indexOfDot = fileName.lastIndexOf("."); - probableFileName = fileName.substring(0, indexOfDot - 1) + "-" + counter + "." + fileName - .substring(indexOfDot + 1); - } else { - probableFileName = fileName + "(" + counter + ")"; - } - probableFile = new File(directory.getAbsolutePath() + File.separator - + probableFileName); - } - fileName = probableFileName; - - return directory.getAbsolutePath() + File.separator - + fileName; - } - - // Guess File extension from the file name - private String guessFileExtensionFromUrl(String url) { - ContentResolver cR = activity.getContentResolver(); - MimeTypeMap mime = MimeTypeMap.getSingleton(); - String type = mime.getExtensionFromMimeType(cR.getType(Uri.parse(url))); - cR.getType(Uri.parse(url)); - return type; - } - -} diff --git a/app/src/main/res/drawable/background_edit_icon_indicator.xml b/app/src/main/res/drawable/background_edit_icon_indicator.xml index 0b3ae873..99c4db92 100644 --- a/app/src/main/res/drawable/background_edit_icon_indicator.xml +++ b/app/src/main/res/drawable/background_edit_icon_indicator.xml @@ -1,6 +1,8 @@ - - + + diff --git a/app/src/main/res/layout/activity_add_shortcut.xml b/app/src/main/res/layout/activity_add_shortcut.xml index 1a3a17e8..1cd78d5b 100644 --- a/app/src/main/res/layout/activity_add_shortcut.xml +++ b/app/src/main/res/layout/activity_add_shortcut.xml @@ -71,18 +71,6 @@ app:layout_constraintEnd_toStartOf="@+id/create" app:layout_constraintTop_toBottomOf="@+id/value_layout" /> - - + app:tint="?colorOnTertiary" /> diff --git a/app/src/main/res/menu/shortcut_icon.xml b/app/src/main/res/menu/shortcut_icon.xml new file mode 100644 index 00000000..3763fe01 --- /dev/null +++ b/app/src/main/res/menu/shortcut_icon.xml @@ -0,0 +1,12 @@ + +

+ + + + + + diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index df52c29d..d8702b1c 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -21,6 +21,10 @@ Назва Обов\'язкове поле Створити + Натисніть щоб змінити зображення + Не вдалося завантажити іконки + Вибрати з галереї + Вибрати з іконок Запуск: %s diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 71a41389..e64754d5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -28,6 +28,10 @@ Label Label cannot be empty Create + Tap to change the icon + Failed to load the icons + Pick from gallery + Select from icons Starting activity: %s diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index f9d7ecdb..ea011599 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -19,7 +19,7 @@ diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 4bdd2900..6daffbc7 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -47,16 +47,35 @@