diff --git a/Ports/Android/src/com/codename1/impl/android/AndroidImplementation.java b/Ports/Android/src/com/codename1/impl/android/AndroidImplementation.java index 52fbbeedb5..c21e4e6b96 100644 --- a/Ports/Android/src/com/codename1/impl/android/AndroidImplementation.java +++ b/Ports/Android/src/com/codename1/impl/android/AndroidImplementation.java @@ -296,8 +296,9 @@ public static void acquirePushWakeLock(long timeout) { } } - private static Context context; - RelativeLayout relativeLayout; + private static Context context; + private static PermissionPromptCallback permissionPromptCallback; + RelativeLayout relativeLayout; final Vector nativePeers = new Vector(); int lastDirectionalKeyEventReceivedByWrapper; private EventDispatcher callback; @@ -10929,9 +10930,36 @@ public boolean isGaussianBlurSupported() { return (!brokenGaussian) && android.os.Build.VERSION.SDK_INT >= 11; } - public static boolean checkForPermission(String permission, String description){ - return checkForPermission(permission, description, false); - } + public static boolean checkForPermission(String permission, String description){ + return checkForPermission(permission, description, false); + } + + public static void setPermissionPromptCallback(PermissionPromptCallback callback) { + permissionPromptCallback = callback; + } + + public static PermissionPromptCallback getPermissionPromptCallback() { + return permissionPromptCallback; + } + + private static String getPermissionText(String key, String defaultValue) { + return UIManager.getInstance().localize(key, Display.getInstance().getProperty(key, defaultValue)); + } + + private static boolean showPermissionPrompt(String permission, String title, String body, String positiveButtonText, String negativeButtonText) { + if (permissionPromptCallback != null) { + return permissionPromptCallback.showPermissionPrompt(permission, title, body, positiveButtonText, negativeButtonText); + } + return Dialog.show(title, body, positiveButtonText, negativeButtonText); + } + + private static void showPermissionMessage(String permission, String title, String body, String okButtonText) { + if (permissionPromptCallback != null) { + permissionPromptCallback.showPermissionMessage(permission, title, body, okButtonText); + return; + } + Dialog.show(title, body, okButtonText, null); + } /** * Return a list of all of the permissions that have been requested by the app (granted or no). @@ -10971,29 +10999,29 @@ public static boolean checkForPermission(String permission, String description, return false; } - String prompt = Display.getInstance().getProperty(permission, description); - String title = Display.getInstance().getProperty("android.permission.ACCESS_BACKGROUND_LOCATION.title", "Requires permission"); - String settingsBtn = Display.getInstance().getProperty("android.permission.ACCESS_BACKGROUND_LOCATION.settings", "Settings"); - String cancelBtn = Display.getInstance().getProperty("android.permission.ACCESS_BACKGROUND_LOCATION.cancel", "Cancel"); - - if(Dialog.show(title, prompt, settingsBtn, cancelBtn)){ - Intent intent = new Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS); - Uri uri = Uri.fromParts("package", getContext().getPackageName(), null); - intent.setData(uri); - getActivity().startActivity(intent); - - String explanationTitle = Display.getInstance().getProperty("android.permission.ACCESS_BACKGROUND_LOCATION.explanation_title", "Permission Required"); - String explanationBody = Display.getInstance().getProperty("android.permission.ACCESS_BACKGROUND_LOCATION.explanation_body", "Please enable 'Allow all the time' in the settings, then press OK."); - String okBtn = Display.getInstance().getProperty("android.permission.ACCESS_BACKGROUND_LOCATION.ok", "OK"); - - Dialog.show(explanationTitle, explanationBody, okBtn, null); - return android.support.v4.content.ContextCompat.checkSelfPermission(getActivity(), permission) == PackageManager.PERMISSION_GRANTED; - } else { - return false; - } - } - - String prompt = Display.getInstance().getProperty(permission, description); + String prompt = getPermissionText(permission, description); + String title = getPermissionText("android.permission.ACCESS_BACKGROUND_LOCATION.title", "Requires permission"); + String settingsBtn = getPermissionText("android.permission.ACCESS_BACKGROUND_LOCATION.settings", "Settings"); + String cancelBtn = getPermissionText("android.permission.ACCESS_BACKGROUND_LOCATION.cancel", "Cancel"); + + if(showPermissionPrompt(permission, title, prompt, settingsBtn, cancelBtn)){ + Intent intent = new Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + Uri uri = Uri.fromParts("package", getContext().getPackageName(), null); + intent.setData(uri); + getActivity().startActivity(intent); + + String explanationTitle = getPermissionText("android.permission.ACCESS_BACKGROUND_LOCATION.explanation_title", "Permission Required"); + String explanationBody = getPermissionText("android.permission.ACCESS_BACKGROUND_LOCATION.explanation_body", "Please enable 'Allow all the time' in the settings, then press OK."); + String okBtn = getPermissionText("android.permission.ACCESS_BACKGROUND_LOCATION.ok", "OK"); + + showPermissionMessage(permission, explanationTitle, explanationBody, okBtn); + return android.support.v4.content.ContextCompat.checkSelfPermission(getActivity(), permission) == PackageManager.PERMISSION_GRANTED; + } else { + return false; + } + } + + String prompt = getPermissionText(permission, description); if (android.support.v4.content.ContextCompat.checkSelfPermission(getContext(), permission) @@ -11008,11 +11036,14 @@ public static boolean checkForPermission(String permission, String description, permission)) { // Show an expanation to the user *asynchronously* -- don't block - if(Dialog.show("Requires permission", prompt, "Ask again", "Don't Ask")){ - return checkForPermission(permission, description, true); - }else { - return false; - } + String title = getPermissionText(permission + ".title", "Requires permission"); + String askAgain = getPermissionText(permission + ".askAgain", "Ask again"); + String dontAsk = getPermissionText(permission + ".dontAsk", "Don't Ask"); + if(showPermissionPrompt(permission, title, prompt, askAgain, dontAsk)){ + return checkForPermission(permission, description, true); + }else { + return false; + } } else { // No explanation needed, we can request the permission. diff --git a/Ports/Android/src/com/codename1/impl/android/AndroidNativeUtil.java b/Ports/Android/src/com/codename1/impl/android/AndroidNativeUtil.java index deb31eda44..6cf5c51795 100644 --- a/Ports/Android/src/com/codename1/impl/android/AndroidNativeUtil.java +++ b/Ports/Android/src/com/codename1/impl/android/AndroidNativeUtil.java @@ -195,9 +195,19 @@ public static void registerViewRenderer(Class viewClass, BitmapViewRenderer b) { viewRendererMap.put(viewClass, b); } - public static interface BitmapViewRenderer { - public Bitmap renderViewOnBitmap(View v, int w, int h); - } + public static interface BitmapViewRenderer { + public Bitmap renderViewOnBitmap(View v, int w, int h); + } + + /** + * Sets a callback used to customize permission rationale dialogs. + * Passing {@code null} restores the default Codename One Dialog behavior. + * + * @param callback callback implementation or {@code null} + */ + public static void setPermissionPromptCallback(final PermissionPromptCallback callback) { + AndroidImplementation.setPermissionPromptCallback(callback); + } /** * Check for a dangerous permission, if the permission is already granted return true, @@ -208,7 +218,7 @@ public static interface BitmapViewRenderer { * @param description show a description to the user why this is needed * @return true if granted false otherwise */ - public static boolean checkForPermission(String permission, String description){ - return AndroidImplementation.checkForPermission(permission, description, false); - } -} + public static boolean checkForPermission(String permission, String description){ + return AndroidImplementation.checkForPermission(permission, description, false); + } +} diff --git a/Ports/Android/src/com/codename1/impl/android/PermissionPromptCallback.java b/Ports/Android/src/com/codename1/impl/android/PermissionPromptCallback.java new file mode 100644 index 0000000000..1dcb37f96d --- /dev/null +++ b/Ports/Android/src/com/codename1/impl/android/PermissionPromptCallback.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.impl.android; + +/** + * Callback used to customize Android permission rationale dialogs. + */ +public interface PermissionPromptCallback { + /** + * Shows a permission prompt that has positive and negative actions. + * + * @param permission Android permission name + * @param title prompt title + * @param body prompt body text + * @param positiveButtonText positive button text + * @param negativeButtonText negative button text + * @return true to perform the positive action, false for the negative action + */ + public boolean showPermissionPrompt(String permission, String title, String body, String positiveButtonText, String negativeButtonText); + + /** + * Shows an informational permission message with only one action button. + * + * @param permission Android permission name + * @param title dialog title + * @param body dialog body + * @param okButtonText button text + */ + public void showPermissionMessage(String permission, String title, String body, String okButtonText); +} diff --git a/docs/demos/android/src/main/java/com/codenameone/developerguide/advancedtopics/PermissionSnippets.java b/docs/demos/android/src/main/java/com/codenameone/developerguide/advancedtopics/PermissionSnippets.java index 0cfe91d52d..3efd813420 100644 --- a/docs/demos/android/src/main/java/com/codenameone/developerguide/advancedtopics/PermissionSnippets.java +++ b/docs/demos/android/src/main/java/com/codenameone/developerguide/advancedtopics/PermissionSnippets.java @@ -7,6 +7,7 @@ import com.codename1.ui.layouts.BoxLayout; import com.codename1.ui.list.MultiButton; import com.codename1.impl.android.AndroidNativeUtil; +import com.codename1.impl.android.PermissionPromptCallback; /** * Snippets related to Android runtime permissions. @@ -52,4 +53,33 @@ public void checkForPermission() { // you have the permission, do what you need // end::androidCheckForPermission[] } + + + public void customizePermissionPromptLocalization() { + // tag::permissionPromptLocalization[] + com.codename1.ui.plaf.UIManager.getInstance().setBundle(new java.util.Hashtable() {{ + put("android.permission.READ_CONTACTS", "Localized rationale for contacts"); + put("android.permission.READ_CONTACTS.title", "Localized permission title"); + put("android.permission.READ_CONTACTS.askAgain", "Localized ask again"); + put("android.permission.READ_CONTACTS.dontAsk", "Localized don't ask"); + }}); + // end::permissionPromptLocalization[] + } + + public void installNativePermissionPromptCallback() { + // tag::androidPermissionPromptCallback[] + AndroidNativeUtil.setPermissionPromptCallback(new PermissionPromptCallback() { + @Override + public boolean showPermissionPrompt(String permission, String title, String body, String positiveButtonText, String negativeButtonText) { + return com.codename1.ui.Dialog.show(title, body, positiveButtonText, negativeButtonText); + } + + @Override + public void showPermissionMessage(String permission, String title, String body, String okButtonText) { + com.codename1.ui.Dialog.show(title, body, okButtonText, null); + } + }); + // end::androidPermissionPromptCallback[] + } + } diff --git a/docs/developer-guide/Advanced-Topics-Under-The-Hood.asciidoc b/docs/developer-guide/Advanced-Topics-Under-The-Hood.asciidoc index a08f773de8..d393ad25ab 100644 --- a/docs/developer-guide/Advanced-Topics-Under-The-Hood.asciidoc +++ b/docs/developer-guide/Advanced-Topics-Under-The-Hood.asciidoc @@ -563,22 +563,35 @@ There are no explicit code changes needed for this functionality to "just work". TIP: Some behaviors that never occurred on Android but were perfectly legal in the past might start occurring with the switch to the new API. E.g. the location manager might be null and your app must always be ready to deal with such a situation -When permission is requested a user will be seamlessly prompted/warned, Codename One has builtin text to control such prompts but you might want to customize the text. You can customize permission text via the `Display` properties e.g. to customize the text of the contacts permission we can do something such as: +When permission is requested a user will be seamlessly prompted/warned. You can customize the permission text via `Display` properties. E.g. to customize the rationale text of the contacts permission: [source,java] ---- include::../demos/android/src/main/java/com/codenameone/developerguide/advancedtopics/PermissionSnippets.java[tag=permissionPrompt,indent=0] ---- -This is optional as there is a default value defined. You can define this once in the `init(Object)` method but for some extreme cases permission might be needed for different things e.g. you might ask for this permission with one reason at one point in the app and with a different reason at another point in the app. +The Android port also checks `UIManager.localize()` for localized values before falling back to `Display` properties. You can provide localized strings for the permission body and dialog buttons/title using keys based on the permission name: + +[source,java] +---- +include::../demos/android/src/main/java/com/codenameone/developerguide/advancedtopics/PermissionSnippets.java[tag=permissionPromptLocalization,indent=0] +---- + +For example, if the permission key is `android.permission.READ_CONTACTS`, you can localize these keys: -The following permission keys are supported: "android.permission.READ_PHONE_STATE" -`android.permission.WRITE_EXTERNAL_STORAGE`, -`android.permission.ACCESS_FINE_LOCATION`, -`android.permission.SEND_SMS`, -`android.permission.READ_CONTACTS`, -`android.permission.WRITE_CONTACTS`, -`android.permission.RECORD_AUDIO`. +* `android.permission.READ_CONTACTS` (dialog body) +* `android.permission.READ_CONTACTS.title` +* `android.permission.READ_CONTACTS.askAgain` +* `android.permission.READ_CONTACTS.dontAsk` + +The same pattern applies to other permissions. `android.permission.ACCESS_BACKGROUND_LOCATION` also supports: +`android.permission.ACCESS_BACKGROUND_LOCATION.settings`, +`android.permission.ACCESS_BACKGROUND_LOCATION.cancel`, +`android.permission.ACCESS_BACKGROUND_LOCATION.explanation_title`, +`android.permission.ACCESS_BACKGROUND_LOCATION.explanation_body`, and +`android.permission.ACCESS_BACKGROUND_LOCATION.ok`. + +This is optional as there is a default value defined. You can define this once in the `init(Object)` method but for some extreme cases permission might be needed for different things e.g. you might ask for this permission with one reason at one point in the app and with a different reason at another point in the app. ===== Simulating Prompts @@ -605,6 +618,15 @@ include::../demos/android/src/main/java/com/codenameone/developerguide/advancedt This will prompt the user with the native UI and later on with the fallback option as described above. Notice that the `checkForPermission` method is a blocking method and it will return when there is a final conclusion on the subject. It uses `invokeAndBlock` and can be safely invoked on the event dispatch thread without concern. +By default, fallback prompts are displayed using Codename One's `Dialog.show(...)`. If you are writing native Android code and need to use your own prompt implementation, you can install a custom callback: + +[source,java] +---- +include::../demos/android/src/main/java/com/codenameone/developerguide/advancedtopics/PermissionSnippets.java[tag=androidPermissionPromptCallback,indent=0] +---- + +Pass `null` to `AndroidNativeUtil.setPermissionPromptCallback()` to restore the default behavior. + === On Device Debugging Codename One supports debugging applications on devices by using the natively generated project. All paid subscription levels include the ability to check an #Include Source# flag in the settings that returns a native OS project. You can debug that project in the respective native IDE.