Mediaprojection issues on Android 9+ - android

I made an OCR application that makes a screenshot using Android mediaprojection and processes the text in this image. This is working fine, except on Android 9+. When mediaprojeciton is starting there is always a window popping up warning about sensitive data that could be recorded, and a button to cancel or start recording. How can I achieve that this window will only be showed once?
I tried preventing it from popping up by creating two extra private static variables to store intent and resultdata of mediaprojection, and reusing it if its not null. But it did not work (read about this method in another post).
// initializing MP
mProjectionManager = (MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE);
// Starting MediaProjection
private void startProjection() {
startActivityForResult(mProjectionManager.createScreenCaptureIntent(), REQUEST_CODE);
}
// OnActivityResult
protected void onActivityResult(final int requestCode, final int resultCode, final Intent data) {
if (requestCode == 100) {
if(mProjectionManager == null) {
cancelEverything();
return;
}
new Handler().postDelayed(new Runnable() {
#Override
public void run() {
if(mProjectionManager != null)
sMediaProjection = mProjectionManager.getMediaProjection(resultCode, data);
else
cancelEverything();
if (sMediaProjection != null) {
File externalFilesDir = getExternalFilesDir(null);
if (externalFilesDir != null) {
STORE_DIRECTORY = externalFilesDir.getAbsolutePath() + "/screenshots/";
File storeDirectory = new File(STORE_DIRECTORY);
if (!storeDirectory.exists()) {
boolean success = storeDirectory.mkdirs();
if (!success) {
Log.e(TAG, "failed to create file storage directory.");
return;
}
}
} else {
Log.e(TAG, "failed to create file storage directory, getExternalFilesDir is null.");
return;
}
// display metrics
DisplayMetrics metrics = getResources().getDisplayMetrics();
mDensity = metrics.densityDpi;
mDisplay = getWindowManager().getDefaultDisplay();
// create virtual display depending on device width / height
createVirtualDisplay();
// register orientation change callback
mOrientationChangeCallback = new OrientationChangeCallback(getApplicationContext());
if (mOrientationChangeCallback.canDetectOrientation()) {
mOrientationChangeCallback.enable();
}
// register media projection stop callback
sMediaProjection.registerCallback(new MediaProjectionStopCallback(), mHandler);
}
}
}, 2000);
}
}
My code is working fine on Android versions below Android 9. On older android versions I can choose to keep that decision to grant recording permission, and it will never show up again. So what can I do in Android 9?
Thanks in advance, I'm happy for every idea you have :)

Well the problem was that I was calling
startActivityForResult(mProjectionManager.createScreenCaptureIntent(), REQUEST_CODE);
every time, which is not necessary (createScreenCaptureIntent() leads to the dialog window which requests user interaction)
My solution makes the dialog appear only once (if application was closed it will ask for permission one time again).
All I had to do was making addiotional private static variables of type Intent and int.
private static Intent staticIntentData;
private static int staticResultCode;
On Activity result I assign those variables with the passed result code and intent:
if(staticResultCode == 0 && staticIntentData == null) {
sMediaProjection = mProjectionManager.getMediaProjection(resultCode, data);
staticIntentData = data;
staticResultCode = resultCode;
} else {
sMediaProjection = mProjectionManager.getMediaProjection(staticResultCode, staticIntentData)};
}
Every time I call my startprojection method, I will check if they are null:
if(staticIntentData == null)
startActivityForResult(mProjectionManager.createScreenCaptureIntent(), REQUEST_CODE);
else
captureScreen();
If null it will request permission, if not it will start the projection with the static intent data and static int resultcode, so it is not needed to ask for that permission again, just reuse what you get in activity result.
sMediaProjection = mProjectionManager.getMediaProjection(staticResultCode, staticIntentData);
Simple as that! Now it will only showing one single time each time you use the app. I guess thats what Google wants, because theres no keep decision checkbox in that dialog like in previous android versions.

Related

Android Making a Call Only Works the Second Time

So I'm making a Speech to Text app using the voice assistant. I'm trying to make a phone call feature so the user can speak a number and it will call it.
I'm almost there but the number only rings the second time I speak it. The first time it says "Call not sent".
I figured out the reason for this is; when the user speaks the number it's not updating the variable first and then calling the "call" function. I've tried almost everything but it doesn't update the variable correctly.
I.e.
private TextView txtSpeechInput;
public String num = "123";
#Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
switch (requestCode) {
case REQ_CODE_SPEECH_INPUT: {
if (resultCode == RESULT_OK && null != data) {
ArrayList<String> result = data
.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS);
txtSpeechInput.setText(result.get(0).replaceAll("\\s+", ""));
num = txtSpeechInput.getText().toString();
}
break;
}
}
}
public void dialPhoneNumber(String phone) {
Intent intent = new Intent(Intent.ACTION_CALL);
intent.setData(Uri.parse("tel:" + phone));
if (intent.resolveActivity(getPackageManager()) != null) {
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.CALL_PHONE) == PackageManager.PERMISSION_GRANTED) {
startActivity(intent);
}
return ;
}
}
private void processResult(String command) {
command = command.toLowerCase();
if(command.indexOf("time") != -1) {
Date now = new Date();
String time = DateUtils.formatDateTime(this, now.getTime(), DateUtils.FORMAT_SHOW_TIME);
speak("The time is " + time);
}
if(command.indexOf("date") != -1) {
String date = DateFormat.getDateInstance().format(new Date());
speak("The date is " + date);
}
else if (command.indexOf("open") != -1) {
if(command.indexOf("browser") != -1) {
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("https://www.google.co.uk/"));
startActivity(intent);
}
}
if(command.indexOf("call") != -1) {
promptSpeechInput();
try {
Thread.sleep(18000);
} catch (InterruptedException e) {
e.printStackTrace();
}
dialPhoneNumber(num);
}
}
In this code, wen the use says "make a call" it opens another prompt to take the speech input. Stores it in txtSpeechInput (Where it says results.get0) and then at that stage I update the "num" variable and convert it to a string.
It then runs dialPhoneNumber
Now let's say I run it the first time and speak "07123456789", it will say call not sent because its trying to call the default 123, if i speak it again or a different number then it will ring the 07123456789.
How and why does it not update before calling the phone feature?
Based on danny117's comment...
promptSpeechInput();
try {
Thread.sleep(18000);
} catch (InterruptedException e) {
e.printStackTrace();
}
dialPhoneNumber(num);
promptSpeechInput starts a new activity, and the result from that activity is the spoken text? If that's right, then why are you prompting for input, sleeping for 18 seconds (never sleep on the main thread by the way), and then assuming your input is ready? As danny says, prompting for input should be the last thing that if block does. Dialing the number should be initiated from onActivityResult.
Also, why is there a default phone number of "123"? That will never be correct, so is not a sensible default. And to reiterate, if you are putting the thread to sleep for a fixed amount of time waiting for something else to happen, you're almost certainly approaching it the wrong way. And if you're putting the main thread to sleep in Android you're absolutely doing it the wrong way.

Attach image on edittext from gallery and camera

I really have no idea how to do it like this, the note is able to attach photo from camera and gallery
Also the other functions like shown on the upper part, I haven't found any tutorial about it. I need help, thank you. I'm a beginner
The scope of this problem may be a bit broad, but I'll try to summarize it as best as I can.
In order to render images from say, a user's third-party gallery app, you'd need to access their device storage first by initially setting the storage permission in your manifest as follows:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
Next, you have to address granting permissions (accessing the user's device storage is considered a dangerous permission) accordingly for Android 6.0/Marshmallow and above at runtime (during installation for Android 6.0 and below) prior to running any UI threads as mentioned here in the docs. Then you open up the gallery app from say, clicking on a Button, and then render an ImageView with bitmap to the URI path of the selected image by using the storage data via a Cursor all within onActivityResult().
Here's a sample Activity of just that:
public class MainActivity extends AppCompatActivity {
// Constant that's used as a parameter to assist with the permission requesting process.
private static final int PERMISSION_CODE = 100;
// Int constant that's used to handle the result back when an image is selected from the
// device's gallery.
private static final int RESULT_LOAD_IMAGE = 1;
private ImageView mImageView;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Requests permission for devices with versions Marshmallow (M)/API 23 or above.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
PERMISSION_CODE);
return;
}
}
// The following invoking method either executes for versions older than M, or until the
// user accepts the in-app permission for the next sessions.
runUi();
}
// Displays a permission dialog when requested for devices M and above.
#Override
public void onRequestPermissionsResult(int requestCode, String[] permissions,
int[] grantResults) {
if (requestCode == PERMISSION_CODE) {
// User accepts the permission(s).
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// Invoker for rendering UI.
runUi();
} else { // User denies the permission.
Toast.makeText(this, "Please come back and then grant permissions!",
Toast.LENGTH_SHORT).show();
// Runs a thread for a slight delay prior to shutting down the app.
Thread mthread = new Thread() {
#Override
public void run() {
try {
sleep(1500);
System.exit(0);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
mthread.start();
}
}
}
private void runUi() {
mImageView = (ImageView) findViewById(R.id.image_view);
// Sets the image button clickable with the following functionality.
findViewById(R.id.change_img_btn).setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
// Instantiates an Intent object for accessing the device's storage.
Intent intent = new Intent(
Intent.ACTION_PICK,
MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
// Triggers the image gallery.
startActivityForResult(intent, RESULT_LOAD_IMAGE);
}
});
}
/**
* Invoked once a third-party app (such as Gallery) is dismissed from its purpose via an
* implicit intent.
*
* #param requestCode is the code constant of the intent's purpose.
* #param resultCode is the result code constant of the intent.
* #param data is the actual intent.
*/
#Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
// Runs the following code should the code constants and intent match that of selecting an
// image from the device's gallery.
if (requestCode == RESULT_LOAD_IMAGE && resultCode == RESULT_OK && data != null) {
// References the device's storage URI for images from the intent parameter.
Uri selectedImageUri = data.getData();
// Initializes a temporary string list of the image file path as the column to render
// the image immediately.
String[] projection = { MediaStore.Images.Media.DATA };
// References and queries the database with the following parameters, and then moves to
// the first row index.
Cursor cursor = getContentResolver().query(
selectedImageUri, // Provider content URI to query
projection, // Columns to include in the resulting Cursor
null, // No selection clause
null, // No selection arguments
null); // Default sort order
cursor.moveToFirst();
// Retrieves and assigns the file path as a string value, and then sets the image's
// bitmap to render it.
String imgFilePath = cursor.getString(cursor.getColumnIndex(projection[0]));
mImageView.setImageBitmap(BitmapFactory.decodeFile(imgFilePath));
// Closes the cursor to release all of its resources.
cursor.close();
}
}
}

Prevent MediaProjection object from being lost on killing the Application

I am using MediaProjection to take screenshot.This is what I am doing.I created an overlay icon using service.On clicking the overlay icon a screenshot is taken.The problem is that whenever the Application is killed either by pressing back button or manually by swiping it the MediaProjection object is lost.Is there a way to maintain the MediaProjection and avoiding requesting for MediaProjection each time application is killed. I have already seen this but I am still unable to do it.
In my Mainactivity Onclick contains startActivityForResult and the resulting onActivityResult is as follows:
#Override
protected void onActivityResult(final int requestCode,final int resultCode,final Intent data)
{
if (requestCode == requestcode)
{
if (resultCode == RESULT_OK)
{
Singelton.setScreenshotPermission((Intent) data.clone());
Singelton.putmanger(mediaProjectionManager);
}else if (resultCode==RESULT_CANCELED) {
Singelton.setScreenshotPermission(null);
}
}
}
The Singelton class is as follows:
protected static void setScreenshotPermission(final Intent permissionIntent)
{
screenshotPermission = permissionIntent;
//screenshotPermission becomes null once the application is killed
}
public static MediaProjection getData()
{
return (mediaProjection);
}
public static void getScreenshotPermission()
{
if (screenshotPermission != null)
{
Log.d("screenshotpermisson", "screenshotPermission != null ");
if(mediaProjection!=null)
{
Log.d("mediaprojection", "mediaprojection != null ");
mediaProjection.stop();
mediaProjection = null;
}
mediaProjection = mediaProjectionManager.getMediaProjection(Activity.RESULT_OK, (Intent) screenshotPermission.clone());
}
else
{
//Here I need to request for media projection again without starting activity
}
}
My Service class for overlay icon handles click as follows:
public void createDisplay()
{
if (mediaProjection == null)
{
Singelton.getScreenshotPermission();
mediaProjection = Singelton.getData();
}
mImageReader = ImageReader.newInstance(width, height, PixelFormat.RGBA_8888, 1);
vd = mediaProjection.createVirtualDisplay("screen-mirror", width, height, mDensity, DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, mImageReader.getSurface(), null, null);
mImageReader.setOnImageAvailableListener(new ImageReader.OnImageAvailableListener(){..}
}
I am new to android and having a hard time figuring this out.Any help will be appreciated.
Just re-create the media projection as fadden implied in his comment. Since every time your service is killed the associated overlay is also killed with it therefore you have to re-create the media projection anyway when restarting the service.
You already doing fine by not consuming the original intent and instead using an clone as answered here https://stackoverflow.com/a/33892817/3918978 .
However if the permission or the MediaProjection shouldn't be valid anymore just get a new one (start an activity asking for it).

Taking a picture from the camera fails 20% of the time

var camera = {
settings : {
quality : 50,
targetWidth : 1024,
targetHeight : 1024,
correctOrientation : true
}
};
var error = function(message) {
alert("Error happened while trying to get a picture", message);
};
document.addEventListener("deviceready", function() {
camera.toFile = function() {
this.settings.destinationType = navigator.camera.DestinationType.FILE_URI;
return this;
},
camera.toBase64 = function() {
this.settings.destinationType = navigator.camera.DestinationType.DATA_URL;
return this;
},
camera.fromCamera = function() {
this.settings.sourceType = navigator.camera.PictureSourceType.CAMERA;
return this;
};
camera.fromLibrary = function() {
this.settings.sourceType = navigator.camera.PictureSourceType.PHOTOLIBRARY;
return this;
};
camera.fromPhotoAlbum = function() {
this.settings.sourceType = navigator.camera.PictureSourceType.SAVEDPHOTOALBUM;
return this;
}
camera.get = function(callback) {
navigator.camera.getPicture(function(data) {
alert("taking a picture successful");
callback(data);
}, error, camera.settings);
};
}, false);
This is my small wrapper for the camera. And I call it like this:
camera.fromPhotoAlbum().toBase64().get(function(base64){});
About 20% of the time, the "alert("taking a picture successful");" is not called, while no error is shown. If I cancel taking a picture, an alert with the message "Error happened while trying to get a picture" is shown, so the error callback works.
Basically nothing happens. I've tested it on a Samsung Galaxy S2 on CM9 and a brand new HTC One X.
There was another question recently about this same problem that I answered. We ran into this at my company and solved it. It has more to do with the Android system than Phonegap.
What's happening is when you start the camera, your app goes into onStop(). While there, the Android system has the right to kill your app if it needs memory. It just so happens that memory usually gets low when the camera takes a picture and dumps it into memory, so there's a good chance your app will get killed while your user takes a picture.
Now that your app is dead, when the camera finishes, it restarts your app. That's why it's acting so weird; the camera comes back into your app, but not the same instance that it had before, so your callback never gets called, since it doesn't exist anymore.
You can reduce the frequency at which this occurs by reducing the quality of the picture and passing it by URI instead of data to your app, but the problem won't go away completely.
To work around the callback never happening, we made a Java callback that starts the camera and saves the picture to the same location every time it takes one. Then, when the app starts back up from getting killed, it looks in that location for the picture.
It's a weird solution to a stupid problem, but if your app gets killed, that camera callback simply won't happen. If you need more information on how to make the Java callback to do this, let me know and I'll put our code up here. Otherwise, take a look at this SO answer for more info.
EDIT: Here's the code we use in our main DroidGap activity:
private static final String folderPath = Environment.getExternalStorageDirectory().getAbsolutePath() + "/appName";
private static final String filePath = "phonegapImage.jpg";
#Override
public void onCreate(Bundle savedState) {
//...The rest of onCreate, this makes the Java available in JavaScript
appView.addJavascriptInterface(this, "Camera");
}
public void takePhoto(final String callback) {
Log.v("Camera Plugin", "Starting takePhoto callback");
Intent intent = new Intent(android.provider.MediaStore.ACTION_IMAGE_CAPTURE);
intent.putExtra(android.provider.MediaStore.EXTRA_OUTPUT, Uri.fromFile(new File(folderPath, filePath)));
startActivityForResult(intent, TAKE_PICTURE);
}
public String getPhotoUri() {
return Uri.fromFile(new File(folderPath, filePath)).toString();
}
#Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
switch (requestCode) {
case TAKE_PICTURE:
if (resultCode == Activity.RESULT_OK) {
//Do whatever you need to do when the camera returns
//This is after the picture is already saved, we return to the page
}
break;
default:
Log.v("Camera", "Something strange happened...");
break;
}
}
Then, in your JavaScript, you can invoke the camera with:
Camera.takePhoto("onPhotoURISuccess");
//Then, to get the location of the photo after you take it and load the page again
var imgPath = Camera.getPhotoUri();
So, that's about it. Just make sure to change all of the path/file/page/etc names to what you want to use in your app. This will overwrite that image every time a picture is taken, but you can probably figure something out to dynamically name them if you don't want that. You can use that URI just as you would any other path in your JavaScript.

How to go back from calling intent

I call camera to take a picture. But I cannot go back to my own original activity after taking the picture. What's the problem? Thank you.
public void addEntry(View view)
{
String EntryName=RegisterName.toString();
Toast.makeText(this, EntryName, Toast.LENGTH_LONG);
Intent addEntryintent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
File file = new File(getFilesDir(),EntryName);
registeryFileUri = Uri.fromFile(file);
addEntryintent.putExtra(MediaStore.EXTRA_OUTPUT, registeryFileUri);
startActivityForResult(addEntryintent,TAKE_PICTURE);
}
#Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == TAKE_PICTURE)
{
if (data != null)
{
Toast.makeText(this, "Successfully Registered!", Toast.LENGTH_LONG);
ImageView Registerimage= (ImageView)findViewById(R.id.RegisterPicture);
Registerimage.setImageURI(registeryFileUri);
}
}
}
It took me a while to get it working and I have made several things and finally it works. I can't tell certainly which of the things I did is the solution to the problem, but they all together form the working solution.
There are multiple reasons why the camera activity does not return back. Two major ones are:
path for the new picture is invalid, or non-existing, or it can't be created
application got suspended and saved path get lost.
So here is my code solving all these problems, all together working.
First I created helper class ImageServices:
class ImageServices {
private static String getTempDirectoryPath(Context ctx) {
File cache;
// SD Card Mounted
if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
cache = new File(Environment.getExternalStorageDirectory().getAbsolutePath() +
"/Android/data/" + ctx.getPackageName() + "/cache/");
}
// Use internal storage
else {
cache = ctx.getCacheDir();
}
// Create the cache directory if it doesn't exist
if (!cache.exists()) {
cache.mkdirs();
}
return cache.getAbsolutePath();
}
public static Uri getOutputImageFileUri(Context ctx) {
// TODO: check the presence of SDCard
String tstamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
File file = new File(getTempDirectoryPath(ctx), "IMG_" + tstamp + ".jpg");
return Uri.fromFile(file);
}
}
The code is partially inspired by developer.android.com and partially by CameraLauncher class of Apache Cordova project.
In my activity the event handler for button to take a picture looks like this:
private Uri imageFileUri;
private static final int MAKE_PHOTO_RESULT_CODE = 100;
private static final int PICK_PHOTO_RESULT_CODE = 101;
public void onMakePhoto(View v) {
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
imageFileUri = ImageServices.getOutputImageFileUri(this);
intent.putExtra(MediaStore.EXTRA_OUTPUT, imageFileUri);
Log.i("babies", "Taking picture: requested " + imageFileUri);
startActivityForResult(intent, MAKE_PHOTO_RESULT_CODE);
}
Method onActivityResult does not really contain much, as imageFileUri already points to the existing file and necessary rendering is done in onResume method, which is called when the activity gets back into foreground:
#Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (resultCode == RESULT_OK) {
switch(requestCode) {
case MAKE_PHOTO_RESULT_CODE:
assert imageFileUri != null;
break;
case ...
...other cases...
break;
}
}
}
But this is still not sufficient, as imageFileUri gets lost as your app gets suspended. And on regular device the chances are close to 100%. So next you need to store the value of imageFileUri to instance state:
#Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
if (imageFileUri == null) {
outState.putString("file-uri", "");
}
else {
outState.putString("file-uri", imageFileUri.toString());
}
};
and load it again in - easiest is directly in onCreate:
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
if (savedInstanceState != null) {
String fileUri = savedInstanceState.getString("file-uri");
if (!fileUri.equals("")) imageFileUri = Uri.parse(fileUri);
}
}
So, again, on top of many other solutions presented on this site as well as elsewhere, there are two major differences:
smarter getTempDirectoryPath inspired by Apache Cordova
allowing imageFileUri to survive suspended application
And now - at least for me - everything works fine.
Answer
Use appContext.getExternalCacheDir() and don't forget to mention permissons.
#Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == TAKE_PICTURE)
{
if(resultCode==Activity.RESULT_OK)
{ //if (data != null)
//{
Toast.makeText(this, "Successfully Registered!", Toast.LENGTH_LONG);
ImageView Registerimage= (ImageView)findViewById(R.id.RegisterPicture);
Registerimage.setImageURI(registeryFileUri);
//}
}
else
Toast.makeText(this, "Not Registered!", Toast.LENGTH_LONG);
}
**"android.permission.CAMERA"**
Check whether the above permission is specified in your manifest or not
Note: It's better to use getExternalCacheDir() than getFilesDir() if you still dont get the
image then use that. Dont forgot to specify the permission "android.permission.WRITE_EXTERNAL_STORAGE" if you use the getExternalCacheDir().
On some devices data will unfortunately be null in onActivityResult after calling the camera activity. So you may need to save your state in your activity's variables, and them read them in onActivityResult. Be sure to save these variables in onSaveInstanceState and restore them in onCreate.

Categories

Resources