I am working on an Application that requires the user to be able to geo-tag picture taken via the camera. The application is to encourage user geo-tag trees to encourage afforestation and reduce global warming.
Is it possible to implement this feature and how can I implement it?
NB: This is my first major project.
Creating a comfortable user experience with location is a big task. These are some of the concerns you'll have to address:
The user may have disabled location globally.
The app may not have the permission to use location.
You want to ask the user for permission and/or to enable location.
You don't want to bother the user by asking over and over again.
Asking for permission means pausing your activity in favor of a system activity that asks for permission. There's a similar but distinct mechanism to ask to enable the location.
Your activity will resume when the user responds, you need to override a special callback method that receives the result.
It's not good to try to fetch location just when you need it. It may take time. Instead you have to ask Android at the outset to push location updates to you.
The user may revoke the location permission while using the app, you must constantly re-check the permissions, be resilient to sudden exceptions, and repeat the request to receive location updates when the location permission comes back.
Here are some snippets from my project that can help you get started:
private val Context.fusedLocationProviderClient get() = getFusedLocationProviderClient(this)
private suspend fun FusedLocationProviderClient.tryFetchLastLocation(): Location? =
lastLocation.await()
?.also { info { "Got response from getLastLocation()" } }
?: run { warn { "getLastLocation() returned null" }; null }
suspend fun Context.canUseLocationFg() =
appHasLocationPermission() &&
locationSettingsException(locationRequestFg, locationRequestBg) == null
import android.Manifest.permission.ACCESS_FINE_LOCATION
import androidx.core.content.PermissionChecker.PERMISSION_GRANTED
fun Context.appHasLocationPermission() =
checkSelfPermission(this, ACCESS_FINE_LOCATION) == PERMISSION_GRANTED
suspend fun Context.locationSettingsException(
vararg locationRequests: LocationRequest
): ApiException? = try {
getSettingsClient(this)
.checkLocationSettings(LocationSettingsRequest.Builder()
.addAllLocationRequests(locationRequests.asList()).build())
.await()
null
} catch (e: ApiException) { e }
const val CODE_REQUEST_FINE_LOCATION = 13
const val CODE_RESOLVE_API_EXCEPTION = 14
suspend fun Fragment.checkAndCorrectPermissionsAndSettings() {
with(context!!) {
if (!appHasLocationPermission()) {
warn { "FG: our app has no permission to access fine location" }
delay(WAIT_MILLISECONDS_BEFORE_ASKING)
if (!appHasLocationPermission()) {
startIntentRequestLocationPermission()
return
}
}
if (locationSettingsException(locationRequestFg, locationRequestBg) == null) return
warn { "FG: ResolvableApiException for location request (probably location disabled)" }
if (!mainPrefs.shouldAskToEnableLocation) return
delay(WAIT_MILLISECONDS_BEFORE_ASKING)
locationSettingsException(locationRequestFg, locationRequestBg)
?.let { it as? ResolvableApiException }
?.also { startIntentResolveException(it) }
}
}
fun Fragment.startIntentRequestLocationPermission() =
requestPermissions(arrayOf(ACCESS_FINE_LOCATION), CODE_REQUEST_FINE_LOCATION)
fun Fragment.startIntentResolveException(e: ResolvableApiException) =
startIntentSenderForResult(e.resolution.intentSender, CODE_RESOLVE_API_EXCEPTION, null, 0, 0, 0, null)
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
if (requestCode != CODE_REQUEST_FINE_LOCATION) return
permissions.zip(grantResults.asList())
.find { (perm, _) -> perm == ACCESS_FINE_LOCATION }
?.also { (_, result) ->
if (result == PermissionChecker.PERMISSION_GRANTED) {
info { "User has granted us the fine location permission" }
} else {
warn { "User hasn't granted us the fine location permission (grant result: ${grantResults[0]})" }
}
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode != CODE_RESOLVE_API_EXCEPTION) return
if (resultCode == Activity.RESULT_OK) {
info { "ResolvableApiException is now resolved" }
} else {
warn { "ResolvableApiException resolution failed with code $resultCode" }
activity!!.mainPrefs.applyUpdate { setShouldAskToEnableLocation(false) }
}
}
val locationRequestFg = LocationRequest().apply {
interval = 1000
fastestInterval = 10
priority = PRIORITY_BALANCED_POWER_ACCURACY
}
suspend fun Context.receiveLocationUpdatesFg(locationState: LocationState) {
fusedLocationProviderClient.apply {
tryFetchLastLocation()?.also {
info { "lastLocation: ${it.description}" }
locationState.location = it
}
LocationCallbackFg.locationState = locationState
requestLocationUpdates(locationRequestFg, LocationCallbackFg, null).await()
info(CC_PRIVATE) { "FG: started receiving location updates" }
}
}
object LocationCallbackFg : LocationCallback() {
var locationState: LocationState? = null
override fun onLocationResult(result: LocationResult) {
val lastLocation = result.lastLocation
info { "FG: received location ${lastLocation.description}" }
locationState?.apply { location = lastLocation }
?: warn { "LocationCallbackFg received an event while not in use" }
}
}
The code relies on Task.await(), this is in Kotlin's library kotlinx-coroutines-play-services.
Related
Thanks for your attention about my trouble.
I am working with android + java & kotlin app.
It needs to be screen recorded but it asks permission every time in android version 12 or above.
Screen Record Permission Request
I hope this would ask only one time for permission request.
I've seen XRecorder does require once for screen recording permission not every time.
Isn't there anyway to make my app to ask only one time for permission request.
Wish someone would help me to solve this.
In my experience, you can save the result of media projection intent and reuse it during the same application process session. That is how most applications overcome this issue.
But be aware that it is more a hack than a solution because the docs do not guarantee it. In contrast, it says you can not reuse it. So use it at your own risk.
From my observations, it will work almost everywhere except some Xiaomi and Huawei devices. You can add them to ignore list and ask every time. Another good idea will be to add a try-catch when reusing an intent, so you can request permission again if the intent is expired.
Context: we use this hack in an application with millions of active users, so it has some credibility.
Code snippet to get the idea, but not production ready:
object MediaProjectionIntentHolder {
var intent: Intent? = null
}
class ScreenRecordingFragment : Fragment() {
// A user clicked button and this function is called
private fun startScreenRecording() {
val intent = MediaProjectionIntentHolder.intent
// You can check Huawei / Xiaomi devices here to ask permission every time
if (intent != null) {
recordScreen(intent)
} else {
requestPermission()
}
}
private fun recordScreen(intent: Intent) {
// Actual screen recording start
}
private showError() {
// show error
}
private fun requestPermission() {
val service = requireContext().getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
val intent = service.createScreenCaptureIntent()
val screenRecordingSupported = context.packageManager.queryIntentActivities(intent, 0).isNotEmpty()
if (screenRecordingSupported) {
startActivityForResult(intent, REQUEST_CODE)
} else {
showError()
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == REQUEST_CODE) {
if (resultCode == Activity.RESULT_OK && data != null) {
MediaProjectionIntentHolder.intent = data
recordScreen(data)
} else {
showError()
}
}
}
private companion object {
private consta val REQUEST_CODE = 42
}
}
I have set up in-app billing on an Android app (java). When I call launchBillingFlow on the BillingClient:
BillingFlowParams billingFlowParams = BillingFlowParams.newBuilder()
.setSkuDetails(skuDetails)
.build();
BillingResult billingResult = billingClient.launchBillingFlow(activity, billingFlowParams);
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
//the system displays the Google Play purchase screen
} else {
Log.e(TAG, "Billing failed: + " + billingResult.getDebugMessage());
}
This is what my onPurchasesUpdated (from PurchasesUpdatedListener) looks like:
#Override
public void onPurchasesUpdated(#NonNull BillingResult billingResult, #Nullable List<Purchase> purchases) {
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK
&& purchases != null) {
for (Purchase purchase : purchases) {
for (String sku : purchase.getSkus()) {
if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED) {
if (skuConsumables.contains(sku)) {
handlePurchaseConsumable(purchase);
} else if (skuNonconsumables.contains(sku)) {
handlePurchaseNonconsumable(purchase);
}
}
}
}
} else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.USER_CANCELED) {
// Handle an error caused by a user cancelling the purchase flow.
billingServiceListener.receivePurchaseError();
} else {
// Handle any other error codes.
billingServiceListener.receivePurchaseError();
}
}
onPurchasesUpdated is called six times, each time with a responseCode of OK. Twice onPurchasesUpdated is called with zero purchases, that's fine. What I am confused about is how to deal with the four times onPurchasesUpdated is called with one purchase. And it seems as though each of these four purchase objects are indistinguishable - the same packageName, acknowledged, orderId, productId, purchaseState, purchaseToken, etc.
To complicate things, for consumable in-app billing, (these are consumable) I am then calling ConsumeResponseListener and onConsumeResponse is also returning four times, each time with responseCode of OK.
private void handlePurchaseConsumable(Purchase purchase) {
ConsumeParams consumeParams =
ConsumeParams.newBuilder()
.setPurchaseToken(purchase.getPurchaseToken())
.build();
ConsumeResponseListener listener = new ConsumeResponseListener() {
#Override
public void onConsumeResponse(BillingResult billingResult, String purchaseToken) {
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
// Handle the success of the consume operation.
}
}
};
billingClient.consumeAsync(consumeParams, listener);
}
Is this behaviour seen by others? I am using static responses while in development, could this be the reason? If people are seeing this, how do you deal with this - do you keep track of what purchase you have attempted, and then when the first response is returned do you register that the purchase was successful and ignore subsequent times that onPurchasesUpdated returns a purchase if you weren't expecting a payment? Though I've seen that Android permits cash purchases with enablePendingPurchases, so that can't be a solution...
This can happen for 2 reasons.
if you are using an outdated version of the google-billing API.
if you forgot to destroy the billing client at the finish of the activity.
I've checked Samples for Google Play In-app Billing and how I can see the main idea of usage the billing client is to use it with activity/fragment lifecycles. So your billing client should implement DefaultLifecycleObserver. We need it to have connect/disconnect billing client use UI lifecycle. In other case we could have few instances of billing client in the App and few listeners that will be triggered with success purchase in same time. So code should looks like:
Activity/Fragment:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycle.addObserver(billingLifecycleManager)
}
BillingLifecycleManager:
class BillingLifecycleManager {
override fun onCreate(owner: LifecycleOwner) {
super.onCreate(owner)
billingClient = BillingClient.newBuilder(app)
.setListener(purchasesUpdatedListener)
.enablePendingPurchases()
.build()
if (billingClient.isReady) {
billingClient.startConnection(...)
}
}
override fun onDestroy(owner: LifecycleOwner) {
super.onDestroy(owner)
if (billingClient.isReady) {
billingClient.endConnection()
}
}
}
I need to develop an app which monitors a list of apps and, if the system is rebooted, return to the last open page of the monitored apps. So, say that android was on the youtube app playing lofi hip hop radio - beats to relax/study to. How could my app send android back to this given video? Also, is there a way for me to know where in the app the user is? For example, can I know that the user was on youtube playing lofi and open it back again for them?
I already know how to open another app as well as detect whichever app is currently open, but I need to know the its uri as well. I can use adb and shell script if it is needed.
I have the following code to launch another app:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
val intent = Intent(this, BackAppListenerService::class.java)
startService(intent)
openApp(this, "com.google.android.youtube")
}
/** Open another app.
* #param context current Context, like Activity, App, or Service
* #param packageName the full package name of the app to open
* #return true if likely successful, false if unsuccessful
*/
fun openApp(context: Context, packageName: String?): Boolean {
val manager = context.packageManager
return try {
val i = manager.getLaunchIntentForPackage(packageName!!)
if (i == null) {
println("Activity not found")
return false;
//throw new ActivityNotFoundException();
}
//throw new ActivityNotFoundException();
i.addCategory(Intent.CATEGORY_LAUNCHER)
context.startActivity(i)
true
} catch (e: ActivityNotFoundException) {
println(e)
false
}
}
}
And with this I get the current foreground app
class BackAppListenerService : Service() {
private var isRunning = false
private var lastApp = ""
#RequiresApi(Build.VERSION_CODES.LOLLIPOP)
override fun onCreate() {
isRunning = true
Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS)
}
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
//Creating new thread for my service
//Always write your long running tasks in a separate thread, to avoid ANR
Thread(Runnable {
while (true) {
try {
Thread.sleep(10)
} catch (e: Exception) {
}
val currentForegroundApp = getForegroundApp()
val currentApp = currentForegroundApp.first
if (currentApp != lastApp) {
// New app on front
lastApp = currentApp
println("Current App $lastApp")
}
}
}).start()
return START_STICKY
}
// Must Have Usage Access Permission
fun getForegroundApp(): Pair<String, UsageStats> {
var currentApp = "NULL"
var currentAppInfo: UsageStats? = null
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
val usm = this.getSystemService(Context.USAGE_STATS_SERVICE) as UsageStatsManager
val time = System.currentTimeMillis()
val appList =
usm.queryUsageStats(UsageStatsManager.INTERVAL_DAILY, time - 1000 * 1000, time)
if (appList != null && appList.size > 0) {
val mySortedMap: SortedMap<Long, UsageStats> =
TreeMap<Long, UsageStats>()
for (usageStats in appList) {
mySortedMap.put(usageStats.lastTimeUsed, usageStats)
}
if (mySortedMap != null && !mySortedMap.isEmpty()) {
currentAppInfo = mySortedMap[mySortedMap.lastKey()]!!
currentApp = mySortedMap[mySortedMap.lastKey()]!!.packageName
}
}
} else {
val am = this.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
val tasks = am.runningAppProcesses
currentApp = tasks[0].processName
}
return Pair(currentApp.split(".").last(), currentAppInfo!!)
}
[...]
}
Any help is much appreciated.
I'm following this docs to implement google pay:
https://developer.android.com/google/play/billing/billing_library_overview
After running this function and purchage a product:
fun buy() {
val skuList = listOf("vip_small")
if (billingClient.isReady) {
val params = SkuDetailsParams
.newBuilder()
.setSkusList(skuList)
.setType(BillingClient.SkuType.INAPP)
.build()
billingClient.querySkuDetailsAsync(params) { responseCode, skuDetailsList ->
if (responseCode == BillingClient.BillingResponse.OK) {
Log.d("lets", "querySkuDetailsAsync, responseCode: $responseCode")
val flowParams = BillingFlowParams.newBuilder()
.setSku("vip_small")
.setType(BillingClient.SkuType.INAPP) // SkuType.SUB for subscription
.build()
val responseCodee = billingClient.launchBillingFlow(this, flowParams)
Log.d("lets", "launchBillingFlow responseCode: $responseCodee")
} else {
Log.d("lets", "Can't querySkuDetailsAsync, responseCode: $responseCode")
}
}
} else {
Log.d("lets", "Billing Client not ready")
}
}
which works fine I want to know if the purchage has been made so I add this code from the cods:
override fun onPurchasesUpdated(#BillingResponse responseCode: Int, purchases: List<Purchase>?) {
if (responseCode == BillingResponse.OK && purchases != null) {
for (purchase in purchases) {
handlePurchase(purchase)
}
} else if (responseCode == BillingResponse.USER_CANCELED) {
// Handle an error caused by a user cancelling the purchase flow.
} else {
// Handle any other error codes.
}
}
But I get the error 'onPurchasesUpdated' overrides nothing
So I remove override and get this "error"
Function "onPurchasesUpdated" is never used
What the hell?? How to call this damn function after the purchage has been made?
I got the solution.
You need to make the activity/fragment use the PurchasesUpdatedListener like:
class VIP : AppCompatActivity(), PurchasesUpdatedListener {
}
Then override will work
If I understand well, once ARCore 1.0 will be released on Google Play, it will be necessary to install it on the device in order to be able to run an ARCore app.
How to check if ARCore lib/apk is installed on device ?
Should be sufficient to do something like:
try {
arCoreSession = Session(this)
val config = Config(arCoreSession)
if (!arCoreSession.isSupported(config)) {
Logger.d("ARCore not installed")
} else {
arCoreSession.configure(config)
}
} catch (ex: Throwable) {
Logger.d("ARCore not installed")
}
This is what I'm using here for one my apps and works fine on devices with or wothout ARCore.
According to ARCore documentation 1.4.0, if optional it is important check its availability recursively and then install it:
void maybeEnableArButton() {
// Likely called from Activity.onCreate() of an activity with AR buttons.
ArCoreApk.Availability availability = ArCoreApk.getInstance().checkAvailability(this);
if (availability.isTransient()) {
// re-query at 5Hz while we check compatibility.
new Handler().postDelayed(new Runnable() {
#Override
public void run() {
maybeEnableArButton();
}
}, 200);
}
if (availability.isSupported()) {
mArButton.setVisibility(View.VISIBLE);
mArButton.setEnabled(true);
// indicator on the button.
} else { // unsupported or unknown
mArButton.setVisibility(View.INVISIBLE);
mArButton.setEnabled(false);
}
}
If already supported just check if ARCore is installed:
// Set to true ensures requestInstall() triggers installation if necessary.
private boolean mUserRequestedInstall = true;
// in onResume:
try {
if (mSession == null) {
switch (ArCoreApk.getInstance().requestInstall(this, mUserRequestedInstall)) {
case INSTALLED:
mSession = new Session(this);
// Success.
break;
case INSTALL_REQUESTED:
// Ensures next invocation of requestInstall() will either return
// INSTALLED or throw an exception.
mUserRequestedInstall = false;
return;
}
}
} catch (UnavailableUserDeclinedInstallationException e) {
// Display an appropriate message to the user and return gracefully.
return;
} catch (...) { // current catch statements
...
return; // mSession is still null
}
Sometimes it is easier to request this with Rx methodology. Here's the code:
private fun getArAvailabilityRx(context: Context): Single<ArCoreApk.Availability> {
return Single.fromCallable<ArCoreApk.Availability> {
ArCoreApk.getInstance().checkAvailability(context)
}.flatMap { availability ->
if (availability.isTransient) {
// `isTransient` means it hasn't finished loading value; let's request the value in 500 ms
getArAvailabilityRx(context).delaySubscription(500, TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread())
} else {
Single.just(availability)
}
}.observeOn(AndroidSchedulers.mainThread())
}
Here's a little utility class I wrote (based originally on something from https://github.com/google/helloargdx).
It will perform all the checks and setup necessary, in order to ensure it is safe to launch a Session.
abstract class ArCheckFragment : Fragment() {
private var userRequestedInstall = true
abstract fun onCameraPermissionDeny()
abstract fun onArCoreUnavailable(availability: Availability)
abstract fun onArCoreInstallFail(exception: UnavailableException)
abstract fun onArCoreInstallSuccess()
override fun onResume() {
super.onResume()
performCheck()
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == REQUEST_CODE_CAMERA_PERMISSION) {
for (i in permissions.indices) {
if (permissions[i] == Manifest.permission.CAMERA &&
grantResults[i] == PackageManager.PERMISSION_GRANTED
) {
checkArCore()
return
}
}
onCameraPermissionDeny()
}
}
/**
* Performs the whole check
*/
fun performCheck() {
if (requestCameraPermission()) {
checkArCore()
}
}
/**
* Requests the camera permission, if necessary.
* #return whether camera permission is already granted. If so, the permission won't be requested.
*/
private fun requestCameraPermission(): Boolean {
if (ContextCompat.checkSelfPermission(
requireContext(),
Manifest.permission.CAMERA
) == PackageManager.PERMISSION_GRANTED
) {
return true
}
requestPermissions(arrayOf(Manifest.permission.CAMERA), REQUEST_CODE_CAMERA_PERMISSION)
return false
}
private fun checkArCore() {
if (!isResumed) {
return
}
val availability = ArCoreApk.getInstance().checkAvailability(activity)
if (availability.isTransient) {
requireView().postDelayed(AR_CORE_CHECK_INTERVAL) { checkArCore() }
return
}
when (availability) {
Availability.SUPPORTED_INSTALLED ->
onArCoreInstallSuccess()
Availability.SUPPORTED_APK_TOO_OLD,
Availability.SUPPORTED_NOT_INSTALLED ->
startArCoreInstallation()
else ->
onArCoreUnavailable(availability)
}
}
private fun startArCoreInstallation() {
try {
val installStatus =
ArCoreApk.getInstance().requestInstall(activity, userRequestedInstall)
when (installStatus) {
InstallStatus.INSTALLED -> onArCoreInstallSuccess()
InstallStatus.INSTALL_REQUESTED,
null ->
// Ensures next invocation of requestInstall() will either return
// INSTALLED or throw an exception.
userRequestedInstall = false
}
} catch (exception: UnavailableException) {
onArCoreInstallFail(exception)
}
}
companion object {
private const val REQUEST_CODE_CAMERA_PERMISSION = 1
private const val AR_CORE_CHECK_INTERVAL = 200L
}
}
You can subclass this Fragment and implement the abstract functions to receive callbacks on what the result of these checks is. Only in onArCoreInstallSuccess is it safe to create a Session.