thanks for checking by. So I've been working on a project which ill expand/add some features to it eventually and currently im working on requesting permissions with Dexter (https://github.com/Karumi/Dexter). I've been able to get it working and when the user clicks on the Add Picture button it also asks the user for permissions, but I've encountered an issue I can't seem to solve myself. So the issue is if the user opens the app for the first time and clicks on the "Add Image" button and then chooses User clicks on add picture button and chooses first option
And lets assume the user denies all permissions, if the user clicks on the button again the app asks for the permissions again but this time with the "deny and dont ask again" option. And I've built a small Dialog that pops up that explains why the permissions are needed and can lead the user to the settings. But I've found out that if the user actually allows the permissions on the second go the app still pops that window and I just wasn't able to solve it.
User allows permissions
Msg still pops even though user gave permissions
Here is my code:
// Creating the variables of Calender Instance and DatePickerDialog listener to use it for date selection
// A variable to get an instance calendar using the default time zone and locale.
private var cal = Calendar.getInstance()
/* A variable for DatePickerDialog OnDateSetListener.
* The listener used to indicate the user has finished selecting a date. It will be initialized later. */
private lateinit var dateSetListener: DatePickerDialog.OnDateSetListener
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_add_happy_place)
// Adds the back button on the ActionBar
setSupportActionBar(toolbar_add_place)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
toolbar_add_place.setNavigationOnClickListener {
onBackPressed()
}
// Initialize the DatePicker and sets the selected date
// https://www.tutorialkart.com/kotlin-android/android-datepicker-kotlin-example/
dateSetListener = DatePickerDialog.OnDateSetListener { _, year, month, dayOfMonth ->
cal.set(Calendar.YEAR, year)
cal.set(Calendar.MONTH, month)
cal.set(Calendar.DAY_OF_MONTH, dayOfMonth)
updateDateInView()
}
// Uses functionality in the onClick function below
et_date.setOnClickListener(this)
tv_add_image.setOnClickListener(this)
}
// This is a override method after extending the onclick listener interface (gets created automatically)
override fun onClick(v: View?) {
when (v!!.id) {
R.id.et_date -> {
DatePickerDialog(
this#AddHappyPlaceActivity, dateSetListener,
cal.get(Calendar.YEAR), cal.get(Calendar.MONTH), cal.get(Calendar.DAY_OF_MONTH)
).show()
}
R.id.tv_add_image -> {
val pictureDialog = AlertDialog.Builder(this)
pictureDialog.setTitle("Select Action")
val pictureDialogItems =
arrayOf("Select photo from gallery", "Capture photo from camera")
pictureDialog.setItems(pictureDialogItems) { _, which ->
when (which) {
0 -> choosePhotoFromGallery()
1 -> Toast.makeText(
this,
"Camera selection coming soon",
Toast.LENGTH_SHORT
).show()
}
}
pictureDialog.show()
}
}
}
// Method used for image selection from GALLERY/PHOTOS
private fun choosePhotoFromGallery() {
// Asking for permissions using DEXTER Library
Dexter.withContext(this).withPermissions(
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.CAMERA
).withListener(object : MultiplePermissionsListener {
override fun onPermissionsChecked(report: MultiplePermissionsReport?) {
// Here after all the permission are granted, launch the gallery to select and image.
if (report!!.areAllPermissionsGranted()) {
Toast.makeText(
this#AddHappyPlaceActivity,
"Storage READ/WRITE permission are granted. Now you can select an image from GALLERY or lets says phone storage.",
Toast.LENGTH_SHORT
).show()
}
}
override fun onPermissionRationaleShouldBeShown(
permissions: MutableList<PermissionRequest>?,
token: PermissionToken?
) {
token?.continuePermissionRequest()
showRationalDialogForPermissions()
}
}).onSameThread().check()
}
// Message to be shown if user denies access and possibly send him to the settings
private fun showRationalDialogForPermissions() {
AlertDialog.Builder(this).setMessage(
"It looks like you have turned off " +
"permissions required for this feature"
).setPositiveButton("GO TO SETTINGS")
{ _, _ ->
try {
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
val uri = Uri.fromParts("package", packageName, null)
intent.data = uri
startActivity(intent)
} catch (e: ActivityNotFoundException) {
e.printStackTrace()
}
}.setNegativeButton("Cancel") { dialog, _ ->
dialog.dismiss()
}.show()
}
// A function to update the selected date in the UI with selected format.
private fun updateDateInView() {
val myFormat = "dd.MM.yyyy"
val sdf = SimpleDateFormat(myFormat, Locale.getDefault())
et_date.setText(sdf.format(cal.time).toString())
}
}
As you can see im talking about the function "showRationalDialogForPermissions()" that gets initalized in the function "onPermissionRationaleShouldBeShown".
If someone knows how to solve this or has any tips that I could give a go I would really appreciate it.
Kind regards,
EDIT: Also I've realised if the user clicks "Deny and don't ask again" and cancels my Dialog, the app doesnt seem to make the Dialog appear after that. Pretty much nothing happens.
So I've tried a few things and I got it somehow working (there are still flaws)
I changed the following:
R.id.tv_add_image -> {
val pictureDialog = AlertDialog.Builder(this)
pictureDialog.setTitle("Select Action")
val pictureDialogItems = arrayOf("Select photo from gallery", "Capture photo from camera")
pictureDialog.setItems(pictureDialogItems) {
_, which ->
when(which) {
0 -> choosePhotoFromGallery()
1 -> Toast.makeText(this, "Camera selection coming soon", Toast.LENGTH_SHORT).show()
}
}
pictureDialog.show()
addButtonClicked += 1
if (addButtonClicked > 2) {
if (ContextCompat.checkSelfPermission(this#AddHappyPlaceActivity,
Manifest.permission.CAMERA)
!= PackageManager.PERMISSION_GRANTED) {
showRationalDialogForPermissions()
}
if (ContextCompat.checkSelfPermission(this#AddHappyPlaceActivity,
Manifest.permission.READ_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
showRationalDialogForPermissions()
}
if (ContextCompat.checkSelfPermission(this#AddHappyPlaceActivity,
Manifest.permission.WRITE_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
showRationalDialogForPermissions()
}
}
}
It's not an optimal solution but I just increment a value i created by 1 and once it reaches 3 (so assumed the user somehow managed to deny 2 times in a row) it pops that window up. Flaws would be that you somehow have to click the cancel button 2 times after giving permissions. But it somehow works and does what i was looking for; if someone has any different ideas, or ways to improve this "solution" it would be much appreciated
Related
I added a feature based on the Codelabs tutorial from Google (https://codelabs.developers.google.com/codelabs/sceneform-intro/index.html?index=..%2F..index#15) which allows users to take photos of AR objects that were added into the scene. The code works fine, however, I wish to hide the PlaneRenderer (the white dots that appear when ARCore detects a surface) in the photo taken by users.
In the onClickListener for the "Capture Photo" button, I tried setting PlaneRenderer to invisible before the takePhoto() is called. This hid the PlaneRenderer on screen, but not in the photo captured.
This is my onClickListener:
capturePhotoBtn.setOnClickListener(new View.OnClickListener(){
#Override
public void onClick(View v) {
arFragment.getArSceneView().getPlaneRenderer().setVisible(false);
for (TransformableNode vNode : videoNodeList){
if (vNode.isSelected()){
vNode.getTransformationSystem().selectNode(null);
}
}
takePhoto();
}
});
videoNodeList contains a list of transformableNodes, and is used to keep track of the objects added by users (as users can add more than 1 object into the scene). As the objects are transformableNodes, users can tap on them to resize/rotate, which shows a small circle underneath the selected object. So, the for-loop added is to de-select all transformableNodes when taking photos, to ensure that the small circle does not appear in the photo as well.
The takePhoto() method is from the CodeLabs tutorial, and is given as follows:
private void takePhoto() {
final String filename = generateFilename();
ArSceneView view = arFragment.getArSceneView();
// Create a bitmap the size of the scene view.
final Bitmap bitmap = Bitmap.createBitmap(view.getWidth(), view.getHeight(),
Bitmap.Config.ARGB_8888);
// Create a handler thread to offload the processing of the image.
final HandlerThread handlerThread = new HandlerThread("PixelCopier");
handlerThread.start();
// Make the request to copy.
PixelCopy.request(view, bitmap, (copyResult) -> {
if (copyResult == PixelCopy.SUCCESS) {
try {
File file = saveBitmapToDisk(bitmap, filename);
MediaScannerConnection.scanFile(this,
new String[] { file.toString() }, null,
new MediaScannerConnection.OnScanCompletedListener() {
public void onScanCompleted(String path, Uri uri) {
Log.i("ExternalStorage", "Scanned " + path + ":");
Log.i("ExternalStorage", "-> uri=" + uri);
}
});
} catch (IOException e) {
Toast toast = Toast.makeText(ChromaKeyVideoActivity.this, e.toString(),
Toast.LENGTH_LONG);
toast.show();
return;
}
Snackbar snackbar = Snackbar.make(findViewById(android.R.id.content),
"Photo saved", Snackbar.LENGTH_LONG);
snackbar.setAction("Open in Photos", v -> {
File photoFile = new File(filename);
Uri photoURI = FileProvider.getUriForFile(ChromaKeyVideoActivity.this,
ChromaKeyVideoActivity.this.getPackageName() + ".provider",
photoFile);
Intent intent = new Intent(Intent.ACTION_VIEW, photoURI);
intent.setDataAndType(photoURI, "image/*");
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
startActivity(intent);
});
snackbar.show();
} else {
Toast toast = Toast.makeText(ChromaKeyVideoActivity.this,
"Failed to copyPixels: " + copyResult, Toast.LENGTH_LONG);
toast.show();
}
handlerThread.quitSafely();
}, new Handler(handlerThread.getLooper()));
}
To give you a clearer picture, PlaneRenderer is hidden on the device screen when the "Capture Photo" button is tapped. This is what is seen immediately after the user taps on the "Capture Photo" button:
However, PlaneRenderer still appears in the photo taken. This is the resulting image that was taken:
which is not what I was looking for as I want to hide the PlaneRenderer in the photo (ie. photo taken should not have the white dots)
Users of this app add objects by selecting an object from the menu and tapping on the PlaneRenderer, so disabling the PlaneRenderer totally is not feasible. In addition, I have another video recording feature in the app that managed to successfully hide the PlaneRenderer in the recording by simply setting PlaneRenderer to invisible, so I am not sure why it doesn't work when capturing photos.
Any help will be greatly appreciated! :)
Finally figured this out after countless of hours. Sharing my solution (may not be the best solution) in case anyone faces this same issue in the future.
I discovered that due to the handlerThread used, the takePhoto() method always happens before the PlaneRenderer was set to invisible whenever the button is tapped. So, I added a short delay to ensure that the reverse happens - ie. delay takePhoto() method for a short while such that the method will always happen after the planeRenderer is invisible.
Here is the code snippet:
capturePhotoBtn.setOnClickListener(new View.OnClickListener(){
#Override
public void onClick(View v) {
arFragment.getArSceneView().getPlaneRenderer().setVisible(false);
for (TransformableNode vNode : videoNodeList){
if (vNode.isSelected()){
vNode.getTransformationSystem().selectNode(null);
}
}
v.postDelayed(new Runnable() {
public void run() {
takePhoto();
}
}, 80);
}
});
This method worked for me, but I am sure there are better solutions to solve this problem. Hope this helps someone with the same problem and feel free to contribute if you know of a better solution.
I have activity one where having a empty textview user has click on this textview to select location from list of location so for that when user click on select location textview it will open list of location with checkbox.
When user select location(can select multiple location) and click on done then all selected location will be showing on activity one textView with all selected checked textview value now when user click on same textview to add more location then on recylerview list all previous checked item should be checked. I'm not getting all previous selected checkbox.
I'm not getting how to achieve this. I need all old checkbox should be selected and user can select some more new checkbox if click on same textview. Please help me to get this. Java code will be also helpful for me
Below is my recylerView Adapter code:-
class SelectMedicineAdapter (val medicineList : ArrayList<String>, val context: Context) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
var selectedCheckBoxMedicineList : ArrayList<String> = ArrayList()
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
holder.itemView.textViewSelectMedicineName.text = medicineList.get(position)
holder.itemView.checkboxSelectMedicine.setOnCheckedChangeListener { buttonView, isChecked ->
val itemText = medicineList.get(position)
if (isChecked) {
selectedCheckBoxMedicineList.add(itemText)
} else {
selectedCheckBoxMedicineList.remove(itemText)
}
}
}
fun getSelectedMedicineList(): ArrayList<String> {
return selectedCheckBoxMedicineList
}
override fun getItemCount(): Int {
return medicineList.size
}
override fun onCreateViewHolder(holder: ViewGroup, p1: Int): RecyclerView.ViewHolder {
val v= (LayoutInflater.from(context).inflate(R.layout.row_select_medicine_adapter,holder,false))
return ViewHolder(v)
}
class ViewHolder (itemView: View): RecyclerView.ViewHolder(itemView){
var textViewSelectMedicineName = itemView.textViewSelectMedicineName
var imageViewPlusButton = itemView.imageViewPlusButton
var imageViewMinusButton = itemView.imageViewMinusButton
var checkboxSelectMedicine = itemView.checkboxSelectMedicine
}
}
You need to update views with their items state (selected or not).
In onSaveInstanceState of your activity/fragment where your adapter is you should write adapters state (which items are selected (getSelectedMedicineList)) to the bundle.
Whenever your fragment/activity is restored just update adapter with data you saved restoreSelectedMedicineList(selectedCheckBoxMedicineList: ArrayList<String>)
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
holder.itemView.textViewSelectMedicineName.text = medicineList.get(position)
holder.itemView.checkboxSelectMedicine.setOnCheckedChangeListener(null)
if(selectedCheckBoxMedicineList.contains(itemText)) {
holder.itemView.setChecked(true)
} else {
holder.itemView.setChecked(false)
}
holder.itemView.checkboxSelectMedicine.setOnCheckedChangeListener { buttonView, isChecked ->
val itemText = medicineList.get(position)
if (isChecked) {
selectedCheckBoxMedicineList.add(itemText)
} else {
selectedCheckBoxMedicineList.remove(itemText)
}
}
}
fun restoreSelectedMedicineList(selectedCheckBoxMedicineList: ArrayList<String>) {
this.selectedCheckBoxMedicineList = selectedCheckBoxMedicineList
notifyDataSetInvalidated()
}
When you start an Activity, it has no knowledge of what is the state of your data. You need to provide the state for the Activity.
By which I mean when the user selects his/her desired locations and goes back to Activity One you to need hold on to these selected locations and when the user again wants to update the locations you need to pass these previously selected locations to the second Activity and then update the RecyclerView's backing data accordingly.
in your list you do maintain a boolean field when select check box then selected position value is true, and notify data and inside onbind viewholder you check first which position is true. if true then show selected checkbox otherwise unselect.
When I'm working with an app, I have faced same problem and also I needed that checked boxes even app closes. So I used SharedPreferences to stored and retrieve values that will indicates states of check boxes then I can easily specify the states of all element. So if you face same problem and have no solution you can use this way.
Just update the model class with a flag on every tick and untick. Check this flag to tick and untick logic for restoring the checkbox state.
I am planning to implement navigation like this:
The problem I face is when user is in LoginFragmennt and presses back button it again loads up LognFragment ie. stuck in loop.
I navigate to LoginnFragment using conditional navigation as per this answer.
How to properly implement this?
IMHO how I do it in my app is a little cleaner. Just add these settings in the nav graph:
<fragment
android:id="#+id/profile_dest"
android:name="com.example.ProfileFragment">
<action
android:id="#+id/action_profile_dest_to_login_dest"
app:destination="#id/login_dest"
app:popUpTo="#+id/profile_dest"
app:popUpToInclusive="true" />
</fragment>
and then navigate to login via
findNavController().navigate(R.id.action_profile_dest_to_login_dest).
popUpTo and popUpToInclusive close ProfileFragment when we navigate to LoginFragment so if the user navigates back, it exits the app.
One of the solutions that i can propose is to override inside your activity onBackPressed method, and finish the activity if your current destination(before on back pressed handled) is login fragment.
override fun onBackPressed() {
val currentDestination=NavHostFragment.findNavController(nav_host_fragment).currentDestination
when(currentDestination.id) {
R.id.loginFragment -> {
finish()
}
}
super.onBackPressed()
}
Here's an official solution suggested by Ian Lake in Navigating navigation video at Jul 23, 2020 on Android Developers YouTube channel. The solution is based on navigation 2.3 release which introduced an ability to return a result to the previous destination.
In our case the login fragment returns LOGIN_SUCCESSFUL state to the previous destination, it might be the profile fragment or any other fragment which requires login.
class LoginFragment : Fragment(R.layout.login) {
...
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val navController = findNavController()
val savedStateHandle = navController.previousBackStackEntry?.savedStateHandle
?: throw IllegalStateException("the login fragment must not be a start destination")
savedStateHandle.set(LOGIN_SUCCESSFUL, false)
// Hook up your UI, ask for login
userRepository.addLoginSuccessListener {
savedStateHandle.set(LOGIN_SUCCESSFUL, true)
navController.popBackStack()
}
}
}
The profile fragment subscribes to the LOGIN_SUCCESSFUL state and processes it. Note that the observer lambda won't be called until the login fragment put a result in and return back to the profile fragment.
class ProfileFragment : Fragment(R.layout.profile) {
...
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val navController = findNavController()
viewLifecycleOwner.lifecycleScope.launchWhenStarted {
userRepository.userFlow.collect { user ->
if (user == null) {
navController.navigate(R.id.login)
}
}
}
val savedStateHandle = navController.currentBackStackEntry?.savedStateHandle
?: throw IllegalStateException()
savedStateHandle.getLiveData<Boolean>(LOGIN_SUCCESSFUL)
.observe(viewLifecycleOwner) { success ->
if (!success) {
// do whathever we want, just for an example go to
// the start destination which doesn't require login
val startDestination = navController.graph.startDestination
navController.navigate(startDestination, navOptions {
popUpTo(startDestination {
inclusive = true
})
})
}
}
}
}
I am building a very basic vocabulary application. The feature I am trying to implement right now is a go to feature, that is taking the user to a specific vocab term. i am doing this by prompting the user with a dialog fragment that asks the user for a page number. (dialog fragment will get triggered via a callback, button press)
This is my code for doing so
public class GoToDialog extends DialogFragment{
submit.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
String pgn = pageNumber.getText().toString();
if(!isNumeric(pgn) || pgn.isEmpty()) {
Toast.makeText(getActivity(), "Please enter a valid number", Toast.LENGTH_SHORT).show();
} else {
int pagina = Integer.parseInt(pgn);
if(pagina <= 0 || pagina > total) {
Toast.makeText(getActivity(), String.format("Please enter a valid " +
"term number between 0 and %d", total), Toast.LENGTH_SHORT).show();
} else {
getDialog().dismiss();
getFragmentManager().executePendingTransactions();
communicator.onDialogMessage(pagina);
}
}
}
});
Here are screenshots when I run my application
Screenshot2(right after screenshot 1)
In terms of functionality The dialog loads up fine and is able to take the user to the right location. However in that example of taking the user from term 7 to term 5, the user is taken to the
right term but the dialog doesn't close as it should from getDialog().dismiss(). I know dismiss is being called because I walked through the code and communicator.onDialogMessage(pagina) returns the right term number to the activity. The dialog does close when I select another term number to go to. Does anyone see the issue? This doesn't make sense to me at all.
To close a dialog, dismiss is the correct method to use
- How to correctly dismiss a DialogFragment?
I also tried what a user suggested in Correct way to remove a DialogFragment: dismiss() or transaction.remove()?, which is to call executePendingTransactions().
If anyone's having a similar issue, the issue with my application was my OnTouchListener.
When I set up on OnTouchListener to trigger the DialogFragment, here was my original code for doing so
goTo - TextView
private void setUpGoToTouchListener() {
goTo.setOnTouchListener(new View.OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event) {
FragmentManager fm = MainActivity.this.getFragmentManager();
GoToDialog dialog = new GoToDialog();
Bundle bundle = new Bundle();
bundle.putInt("Size", defMan.getTotalCount());
dialog.setArguments(bundle);
dialog.show(fm, "Manager");
return true;
}
});
}
When the gesture(a touch) on the TextView occurs, two MotionEvents will be generated, the press, ACTION_DOWN - first finger has touched the screen, and the release, ACTION_UP - the last of the fingers has stopped touching the screen. Because two motion events occurred, two dialog fragments were created. Thats why dismiss had to be called twice in my situation to get rid of both dialog fragments. I fixed this by having a conditional test for event.getAction()
Fragments are NOT essential to my question (so don't leave lol), but I mention it to explain why I am trying to do this.
I am using fragments so depending on layout a different activity will be the container. Because of that I need this routine available to multiple activities. I have a common routine that needs to run regardless of which activity is used and so in order to not duplicate code I set up the routine to run from the application object.
This code works if it is contained in the activity, but when put in the application object (and modified as necessary) it fails. When I attempt to .show() the dialog I get the error "Unable to add window -- token null is not for an application".
This is the calling routine from one of the activities that needs to call the failing routine:
#Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
NavUtils.navigateUpTo(this, new Intent(this, ChecklistListActivity.class));
return true;
case R.id.mnuDelete:
((KnowUrStuffApp)getApplication()).deleteChecklist(this);//<--This is the call!!!
return true;
default:
return super.onOptionsItemSelected(item);
}
}
This is the routine contained in my application subclass:
public void deleteChecklist(final FragmentActivity sender){
Checklist cl = getDbHelper().getCurrentChecklist();
if (cl == null)
Toast.makeText(this, getString(R.string.strSelectAChecklistToDelete), Toast.LENGTH_SHORT).show();
else {
try {
new AlertDialog.Builder(this)
.setMessage(cl.getChecklistTitle() + " " + getString(R.string.strConfirmDelete))
.setCancelable(true)
.setPositiveButton(android.R.string.ok,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int whichButton){
performDeleteChecklist();
if (sender instanceof ChecklistDetailActivity)
NavUtils.navigateUpTo(sender, new Intent(sender, ChecklistListActivity.class));
}
})
.setNegativeButton(android.R.string.cancel, null)
.show();//<--This causes exception!
} catch (Exception e) {
Log.e(TAG,e.getLocalizedMessage());
Toast.makeText(this, e.getLocalizedMessage(), Toast.LENGTH_LONG * 4).show();
}
}
}
How can I get this to work, or if this is a total wrong way to have this routine available to multiple activities, how could I make it available?
I could duplicate the code in each activity to make it work, but then I'll have to remember to update both whenever I make changes. Further I will need several more routines that I will need to do the same thing for so I really need to figure out how to make my routine available to multiple activities.
Thank you guys so much! :-D
Try this. Change this line:
new AlertDialog.Builder(this)
to:
new AlertDialog.Builder(sender)