پیاده سازی قابلیت Runtime Permission

آموزش کار با Runtime Permission در اندروید

در اندروید ۶ (Marshmallow) قابلیت امنیتی جدیدی با نام Runtime Permission به سیستم عامل اندروید اضافه شد. با معرفی این قابلیت، از اندروید ۶ و به بالا کاربر بجای مشاهده و تایید دسته جمعی مجوزهای موردنیاز برنامه در هنگام نصب، پس از نصب اپلیکیشن تعیین می‌کند برنامه مجوز دسترسی به کدامیک از امکانات را داشته باشد. در این جلسه به نحوه پیاده سازی قابلیت Runtime Permisson در اندروید ۶ و به بالا می‌پردازیم.

Runtime Permission چیست؟

به نام خدا. اگر تجربه کار با یک موبایل یا تبلت اندرویدی قدیمی یعنی قبل از اندروید مارشمالو را دارید، حتما لیست مجوزهای مورد نیاز اپلیکیشن‌ها هنگام نصب را بخاطر دارید. مجوزهایی مانند دسترسی به مخاطبین، دوربین، ارسال و دریافت پیامک، بلوتوث، وای فای و… .

پذیرش مجوزهای اندروید قبل از نصب اپلیکیشن

قبل از آغاز عملیات نصب برنامه روی دیوایس اندرویدی، لیستی از تمامی دسترسی‌هایی که توسط آن از سیستم عامل اخذ خواهد شد برای کاربر نمایش داده می‌شد. در تصویر بالا کاربر قبل از آغاز نصب برنامه باید دسترسی به مخاطبین، موقعیت مکانی و میکروفون را تایید کند. در اینجا دو مشکل اساسی وجود داشت که می‌بایست چاره‌ای برای آن اندیشیده می‌شد:

۱: کاربر دو انتخاب داشت. باید همه دسترسی‌ها را می‌پذیرفت تا بتواند برنامه مدنظر خود را نصب کند در غیر اینصورت اگر حتی یک مورد از مجوزها باب میلش نبود می‌بایست از نصب و استفاده از آن برنامه به طور کامل صرف نظر می‌کرد.
۲: به دلیل طولانی بودن لیست مجوزها و همچنین دریافت یکباره آنها، اکثر اشخاص بخصوص کاربرانی که اطلاعات کمتری در مورد ماهیت مجوزها و ارتباط آن با حریم شخصی خود داشتند، توجه زیادی به نوع مجوزهای دریافتی نداشته و بدون مطالعه دقیق لیست، اقدام به تایید و نصب نرم افزارها روی دیوایس می‌کردند که نتیجه آن چیزی جز به خطر افتادن حریم شخصی افراد نبود.

سرانجام گوگل همزمان با معرفی اندروید Marshmallow در اکتبر ۲۰۱۵ ویژگی جدیدی برای رفع این مشکل به سیستم عامل اندروید اضافه کرد. این قابلیت Runtime Permission نام داشت؛ یعنی “اخذ مجوز هنگام اجرا“.
بنابراین حالا دیگر خبری از لیست مجوزها در هنگام نصب برنامه نیست و کاربر ابتدا برنامه‌های مدنظر خود را بدون تایید هیچگونه مجوز حساسی نصب می‌کند. بعد از اتمام نصب بنا به سلیقه و صلاح دید توسعه دهنده برنامه، مجوزها می‌توانند هنگام اولین اجرا و یا صرفا هنگامی که لازم هست از کاربر اخذ گردد.
در روش گذشته مدیریت مجوزها برای توسعه دهنده و برنامه نویس اپلیکیشن‌های اندرویدی ساده‌تر بود و تنها کاری که باید انجام می‌شد، تعریف مجوزها در مانیفست بود. اما در عوض کاربر کنترلی بر روی مجوزها نداشت. مسلم است که در اینجا باید رضایت کاربر در اولویت قرار می‌گرفت.

درخواست تایید مجوزهای خطرناک هنگام اجرای برنامه (Runtime Permission)

یک اپلیکیشن شبکه اجتماعی مانند Twitter یا Instagram را درنظر بگیرید. هنگام نصب هیچ مجوزی از شما دریافت نمی‌شود اما به محض اینکه بخواهید برای اولین بار از گالری تصویر یک عکس را انتخاب و ارسال کنید، قبل از آنکه برنامه بتواند به گالری عکس دسترسی داشته باشد درخواستی برای تایید یا رد مجوز دسترسی برنامه به محتوای گالری نمایش داده می‌شود. یا در اولین اتصال برنامه به دوربین، درخواست تایید یا رد دسترسی به دوربین صادر خواهد شد.
اینکه درخواست مجوز چه هنگامی به کاربر نمایش داده شود به انتخاب و استراتژی توسعه دهنده برمی‌گردد. یعنی این عملیات به طور خودکار انجام نمی‌شود و لازم است برنامه نویس آن را کنترل کند در غیر اینصورت برنامه هنگام نیاز به دسترسی به یک مجوز خاص، دچار مشکل شده و کرش (Crash) می‌کند.

نکته: برای دیوایس‌های قبل از اندروید ۶ کماکان مجوزهای تعریف شده در مانیفست برنامه هنگام نصب از کاربر اخذ می‌گردد. در واقع درخواست مجوز بعد از نصب تنها در اندروید ۶ و بالاتر صورت می‌پذیرد.

انواع دسترسی‌ها در سیستم عامل اندروید

در اندروید مجوزهای دسترسی به قابلیت‌های سخت افزاری و نرم افزاری به دو دسته‌ی مجوزهای نرمال (Normal) و مجوزهای خطرناک یا حساس (Dangerous) تقسیم بندی می‌شود که در ادامه توضیحات لازم را ارائه می‌دهم.

مجوزهای نرمال:

به آن دسته از مجوزهایی گفته می‌شود که به صورت خودکار و هنگام نصب از طرف سیستم عامل به برنامه داده شده و نیازی به دریافت موافقت و تایید آن از کاربر نیست. مانند مجوز دسترسی به اینترنت و بلوتوث.
لیست زیر شامل مجوزهای نرمال است:

WRITE_SYNC_SETTINGS
ACCESS_LOCATION_EXTRA_COMMANDS
ACCESS_NETWORK_STATE
ACCESS_NOTIFICATION_POLICY
ACCESS_WIFI_STATE
BLUETOOTH
BLUETOOTH_ADMIN
BROADCAST_STICKY
CALL_COMPANION_APP
CHANGE_NETWORK_STATE
CHANGE_WIFI_MULTICAST_STATE
CHANGE_WIFI_STATE
DISABLE_KEYGUARD
EXPAND_STATUS_BAR
FOREGROUND_SERVICE
GET_PACKAGE_SIZE
INSTALL_SHORTCUT
INTERNET
KILL_BACKGROUND_PROCESSES
MANAGE_OWN_CALLS
MODIFY_AUDIO_SETTINGS
NFC
NFC_TRANSACTION_EVENT
READ_SYNC_SETTINGS
READ_SYNC_STATS
RECEIVE_BOOT_COMPLETED
REORDER_TASKS
REQUEST_COMPANION_RUN_IN_BACKGROUND
REQUEST_COMPANION_USE_DATA_IN_BACKGROUND
REQUEST_DELETE_PACKAGES
REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
REQUEST_PASSWORD_COMPLEXITY
SET_ALARM
SET_WALLPAPER
SET_WALLPAPER_HINTS
TRANSMIT_IR
USE_BIOMETRIC
USE_FINGERPRINT
USE_FULL_SCREEN_INTENT
WAKE_LOCK
WRITE_SYNC_SETTINGS

مجوزهای خطرناک یا حساس:

مجوزهایی هستند که از لحاظ امنیتی در سطح بالاتری قرار داشته و تا زمانی که کاربر با دسترسی به آنها موافقت نکند اپلیکیشن اجازه دسترسی به هیچکدام را نخواهد داشت. مانند مجوز دسترسی به دوربین، فهرست مخاطبین، سنسورها و… .
لیست زیر شامل مجوزهای خطرناک اندروید است:

ACCEPT_HANDOVER
ACCESS_BACKGROUND_LOCATION
ACCESS_COARSE_LOCATION
ACCESS_FINE_LOCATION
ACCESS_MEDIA_LOCATION
ACTIVITY_RECOGNITION
ADD_VOICEMAIL
ANSWER_PHONE_CALLS
BODY_SENSORS
CALL_PHONE
CAMERA
GET_ACCOUNTS
PROCESS_OUTGOING_CALLS
READ_PHONE_NUMBERS
READ_PHONE_STATE
READ_SMS
RECEIVE_SMS
RECEIVE_MMS
RECEIVE_WAP_PUSH
RECORD_AUDIO
USE_SIP
WRITE_CALENDAR
READ_CALENDAR
WRITE_CALL_LOG
READ_CALL_LOG
WRITE_CONTACTS
READ_CONTACTS
READ_EXTERNAL_STORAGE
WRITE_EXTERNAL_STORAGE
نکته: چه مجوزهای نرمال و چه خطرناک هردو باید درون مانیفست پروژه تعریف شوند. مهم نیست مجوزهای مورد نیاز ما جزء کدام دسته هستند. در هر صورت باید تمام حق دسترسی‌های لازم را درون AndroidManifest.xml تعریف کنیم. اما برای مجوزهای خطرناک می‌بایست مورد به مورد تاییدیه آن از کاربر دریافت شود.

فکر می‌کنم توضیحات فوق کفایت می‌کند. در ادامه و در قالب دو پروژه مجزا، ویژگی ران تایم پرمیشن (Runtime Permission) را به صورت عملی از حالت ساده تا پیشرفته بررسی می‌کنیم. هر سطح را در یک پروژه جداگانه انجام می‌دهم تا درک آن و همچنین دسترسی به سورس کدها ساده‌تر باشد.

پروژه شماره ۱: درخواست یک مجوز از کاربر توسط قابلیت Runtime Permission

در این قسمت ویژگی Runtime Permission را در ساده ترین حالت بررسی و تمرین می‌کنیم. در این پروژه فقط یک مجوز از کاربر درخواست می‌شود که اگر آنرا تایید یا رد کند، پیغام متناسب با آنرا دریافت خواهد نمود.
یک پروژه جدید در اندروید استودیو با نام Runtime Permission و یک Empty Activity ایجاد می‌کنم. همچنین زبان Java را برای پروژه انتخاب کردم.
ابتدا یک Button در layout اکتیویتی می‌سازم تا با کلیک روی آن، درخواست مجوز اجرا شود:

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/btn_request"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:layout_centerVertical="true"
        android:text="Request Permission" />
    
</RelativeLayout>

در جلسه گذشته یعنی آموزش کار با Camera2 API در اندروید مجوز دسترسی به دوربین (CAMERA) و نوشتن روی کارت حافظه (WRITE_EXTERNAL_STORAGE) را از کاربر دریافت کردیم. در این جلسه هم از همین مجوزها استفاده می‌کنم.
در پروژه شماره ۱ فقط یک مجوز را از کاربر دریافت می‌کنم بنابراین مجوز دسترسی به دوربین را در مانیفست تعریف کردم:

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="ir.android_studio.runtimepermission">

    <uses-permission android:name="android.permission.CAMERA"/>
    
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

در مرحله بعد رویداد setOnClickListener دکمه را تعریف کرده و یک شرط درون آن قرار می‌دهم:

MainActivity.java

package ir.android_studio.runtimepermission;

import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.ContextCompat;

import android.Manifest;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity {

    private Button requestButton;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        requestButton = findViewById(R.id.btn_request);

        requestButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {

                if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {

                    requestCameraPermission();

                } else {

                    Toast.makeText(MainActivity.this, "مجوز قبلا دریافت شده", Toast.LENGTH_SHORT).show();

                }

            }
        });

    }
}

این شرط چک می‌کند اگر مجوز دسترسی به دوربین قبلا توسط کاربر تایید نشده، متد requestCameraPermission اجرا و در غیر اینصورت یک پیغام از جنس Toast نمایش داده شود با این مضمون که مجوز دسترسی قبلا دریافت شده است. که در یک پروژه واقعی، بجای این Toast کد مربوط به اتصال برنامه به دوربین را می‌نویسیم.

ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED

برای بررسی فعال بودن یا نبودن مجوز مدنظر از متد checkSelfPermission استفاده می‌کنیم. این شرط دائما و برای هربار استفاده از دوربین باید اجرا شده و وضعیت را بررسی نماید زیرا ممکن است کاربر بعد از تایید یک مجوز، از طریق تنظیمات برنامه و به صورت دستی آنرا غیرفعال کند. این متد دو پارامتر دارد. پارامتر اول کانتکست و پارامتر دوم مجوز یا Permission مدنظر باید تعریف شود. مجوزها با فرمت زیر تعریف می‌شوند:

Manifest.permission.PERMISSION_NAME

در واقع همان مجوزی که قبلا در مانیفست پروژه تعریف کردیم اینجا نیز باید درخواست شود.

متد checkSelfPermission بعد از بررسی وضعیت مجوز، دو مقدار را برمی‌گرداند:

  • PERMISSION_GRANTED: واژه Granted یعنی “موافقت شده”. بنابراین هنگامی که مجوز مدنظر قبلا تایید شده باشد این مقدار را برمی‌گرداند.
  • PERMISSION_DENIED: واژه Denied یعنی “رد شده”. بنابراین هنگامی که مجوز مدنظر قبلا تایید نشده باشد این مقدار را برمی‌گرداند.

لذا در این شرط بررسی می‌کنیم اگر برای مجوز CAMERA نتیجه‌ی “موافقت شده” برنگشت، یعنی برابر نبود (!=) با PERMISSION_GRANTED، متدی که با نام دلخواه requestCameraPermission نوشته‌‌ام را اجرا کن در غیر اینصورت اگر مقدار برگشتی PERMISSION_GRANTED بود، پیغام Toast مدنظر را نمایش بده.
روی متد requestCameraPermission کلیدهای ترکیبی alt + enter را می‌زنم تا متد بصورت خودکار و بدون نیاز به نوشتن دستی به اکتیویتی اضافه شود:

اضافه کردن متد requestPermission به اکتیویتی توسط کلیدهای alt+enter

اضافه کردن متد requestPermission به اکتیویتی توسط کلیدهای alt+enter

متد درون کلاس MainActivity و بعد از متد onCreate ساخته شد:

private void requestCameraPermission() { }

متد را به اینصورت کامل می‌کنم:

private void requestCameraPermission() {

    if (ActivityCompat.shouldShowRequestPermissionRationale(MainActivity.this, Manifest.permission.CAMERA)) {

        new AlertDialog.Builder(this)
                .setTitle("درخواست مجوز")
                .setMessage("برای دسترسی به دوربین باید مجوز را تایید کنید")
                .setPositiveButton("موافقم", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialogInterface, int i) {

                        reqPermission();

                    }
                })
                .setNegativeButton("لغو", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialogInterface, int i) {

                        dialogInterface.dismiss();

                    }
                })
                .create()
                .show();

    } else {

        reqPermission();

    }

}

در اینجا ما باید تاییدیه مجوز دسترسی به دوربین را از کاربر دریافت کنیم. قبل از درخواست مستقیم مجوز، از یک متد دیگر برای مدیریت بهتر Runtime Permission و به عبارتی بهبود UX یا همان تجربه کاربری استفاده می‌کنم. با استفاده از متد shouldShowRequestPermissionRationale یک شرط تعریف می‌کنیم. اگر دستور دریافت مجوز را مستقیما درون متد requestCameraPermission تعریف کنم ممکن است برای کاربر آماتور باعث بروز ابهام در استفاده از برنامه شود.
فرض کنید شخص قصد دارد توسط اپلیکیشن شبکه اجتماعی خود برای اولین بار عکس بگیرد. درخواست تایید مجوز را مشاهده می‌کند اما چرایی دسترسی برنامه به دوربین برایش واضح نیست. یا اصلا متن درخواست را به درستی مطالعه نکرده و طبق عادت گزینه DENY یعنی لغو را انتخاب می‌کند. طبیعتا برنامه به دوربین متصل نشده. دوباره داخل برنامه سعی می‌کند تا بتواند یک تصویر ثبت کند اما باز هم درخواست مجوز تکرار می‌گردد. اینجا متد shouldShowRequestPermissionRationale وارد عمل می‌شود. با استفاده از این متد می‌توانیم تعیین کنیم اگر کاربر قبلا یک بار درخواست مجوزی را رد کرده، برای دفعات بعد، قبل از درخواست مجدد مجوز ابتدا یک پیغام حاوی توضیحات لازم نیز نمایش داده شود. کاربرد این متد از ترجمه تحت الفظی نام آن نیز مشخص می‌شود: “باید علت درخواست مجوز نمایش داده شود”. فکر می‌کنم حالا ماهیت شرطی که تعریف شده برایتان روشن شده. در اینجا تعریف کردیم اگر shouldShowRequestPermissionRationale مقدار true برگرداند، به عبارتی اگر لازم است توضیحاتی به کاربر ارائه شود، قسمت اول شرط و در غیر اینصورت قسمت دوم شرط را اجرا شود. در قسمت اول شرط یک AlertDialog تعریف کردم که یک توضیح خلاصه را به کاربر نمایش می‌دهد. سپس دو دکمه “موافقم” و “لغو” اضافه شده. اگر گزینه موافق را انتخاب کند متد reqPermission اجرا خواهد شد در غیر اینصورت دیالوگ بسته شده و اتفاقی نمی‌افتد. اگرهم نیازی به نمایش توضیحات نیست و اولین بار است که این مجوز درخواست می‌شود، بدون هیچ عمل اضافه‌ای متد reqPermission را اجرا کن.
حالا باید دستور مربوط به دریافت مجوز را درون متد reqPermission تعریف کنم. روی یکی از آنها alt + enter زده و متد را به اکتیویتی اضافه می‌کنم. سپس دستور درخواست مجوز را می‌نویسم:

private void reqPermission() {

    ActivityCompat.requestPermissions(MainActivity.this, new String[] {Manifest.permission.CAMERA}, CAMERA_REQUEST_CODE);

}

برای درخواست مجوز از متد requestPermissions استفاده می‌شود. این متد سه پارامتر می‌پذیرد. پارامتر اول کلاس اکتیویتی، پارامتر دوم مجوز مدنظر که در قالب یک String[] تعریف شده و پارامتر سوم یک کد جهت بررسی نتیجه درخواست است. این کد را قبلا درون اکتیویتی تعریف کردم. کد کامل MainActivity را بررسی کنید:

package ir.android_studio.runtimepermission;

import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;

import android.Manifest;
import android.content.DialogInterface;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity {

    private final int CAMERA_REQUEST_CODE = 100;
    private Button requestButton;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        requestButton = findViewById(R.id.btn_request);

        requestButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {

                if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {

                    requestCameraPermission();

                } else {

                    Toast.makeText(MainActivity.this, "مجوز قبلا دریافت شده", Toast.LENGTH_SHORT).show();

                }

            }
        });
    }

    private void requestCameraPermission() {

        if (ActivityCompat.shouldShowRequestPermissionRationale(MainActivity.this, Manifest.permission.CAMERA)) {

            new AlertDialog.Builder(this)
                    .setTitle("درخواست مجوز")
                    .setMessage("برای دسترسی به دوربین باید مجوز را تایید کنید")
                    .setPositiveButton("موافقم", new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialogInterface, int i) {

                            reqPermission();

                        }
                    })
                    .setNegativeButton("لغو", new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialogInterface, int i) {

                            dialogInterface.dismiss();

                        }
                    })
                    .create()
                    .show();

        } else {

            reqPermission();

        }

    }

    private void reqPermission() {

        ActivityCompat.requestPermissions(MainActivity.this, new String[] {Manifest.permission.CAMERA}, CAMERA_REQUEST_CODE);

    }
}

نیاز به توضیح نیست که مقدار تعیین شده برای CAMERA_REQUEST_CODE کاملا اختیاری می‌باشد.
در نهایت برای مدیریت نتیجه (result) درخواست مجوز لازم است متد onRequestPermissionsResult را درون اکتیویتی Override کنیم:

استفاده از متد onRequestPermissionsResult برای تعیین نتیجه درخواست مجوز

متد به اکتیویتی اضافه می‌شود:

@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}

برای بررسی نتیجه درخواست از کد CAMERA_REQUEST_CODE استفاده می‌کنیم. پارامتر نخست onRequestPermissionsResult از نوع int و با نام requestCode است. همچنین تعداد مجوزهای تایید شده (granted) نیز درون پارامتر grantResults ذخیره می‌گردد. متد را تکمیل می‌کنم:

@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {

    if (requestCode == CAMERA_REQUEST_CODE) {

        if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {

            Toast.makeText(this, "مجوز تایید شد", Toast.LENGTH_SHORT).show();

        } else {

            Toast.makeText(this, "مجوز رد شد", Toast.LENGTH_SHORT).show();

        }

    }

}

ابتدا بررسی می‌شود کد موجود در requestCode با کد مجوز مدنظر ما یکسان باشد. سپس درون این شرط یک شرط دیگر قرار دارد. شرط دوم بررسی می‌کند اولا طول (length) grantResults بزرگتر از صفر باشد (یعنی حداقل یک مجوز تایید شده باشد) و ثانیا درخواست مجوز نخست (یعنی موقعیت صفر) نیز تایید شده باشد که در این پروژه ما فقط یک مجوز درخواست کرده‌ایم. حالا اگر شرط برقرار بود، پیغام “مجوز تایید شد” و در غیر اینصورت پیغام “مجوز رد شد” اجرا شود.
خب! حالا پروژه را روی یک دیوایس با API 23 (اندروید ۶) یا بالاتر اجرا می‌کنم:

درخواست صدور مجوز دسترسی به دوربین

نمایش دیالوگ ران تایم پرمیشن

مشاهده می‌کنید با کلیک روی دکمه درخواست مجوز، دیالوگی با آیکون دوربین اجرا می‌شود که از کاربر می‌خواهد دسترسی جهت ثبت عکس یا ضبط ویدئو را به برنامه بدهد. روی گزینه ALLOW کلیک می‌کنم:

Permissions granted

مجوز دسترسی به دوربین برای برنامه صادر شده و پیغام Toast مرتبط با آن نیز اجرا شد. حالا دوباره روی دکمه کلیک می‌کنم:

مجوزها قبلا تایید شده است

پیغام “مجوز قبلا دریافت شده” اجرا می‌شود.
اگر بخاطر داشته باشید گفتیم متد checkSelfPermission باید هر بار اجرا شود تا بررسی کند آیا مجوز فعال است یا خیر. ممکن است بگویید اگر کاربر یکبار مجوز را تایید کند دیگر امکان غیر فعال کردن برایش وجود ندارد. در صورتی که اینطور نیست و در قسمت مدیریت اپلیکیشن‌ها در اندروید لیست مجوزهای هر اپ نمایش داده می‌شود:

تنظیمات اپلیکیشن در اندروید

روی Permissions کلیک می‌کنم:

مجوزهای دسترسی برنامه ها در سیستم عامل اندروید

ملاحظه می‌کنید مجوز دسترسی به CAMERA در اینجا فعال است که کاربر می‌تواند آنرا غیر فعال کند.
حالا می‌خواهم حالت دوم را تست کنم. یعنی هنگامی که کاربر مجوز درخواستی Runtime Permission را تایید نکرده و گزینه DENY را انتخاب کند. ابتدا اپ فعلی را از روی دیوایس Uninstall می‌کنم تا تنظیمات قبلی برنامه کاملا حذف شود. سپس دوباره پروژه را Run کرده و روی دکمه کلیک میکنم. اینبار گزینه DENY را انتخاب می‌کنم:

رد کردن درخواست مجوز در runtime permission

واکنش بعد از DENY کردن درخواست مجوز

پیغام “مجوز رد شد” اجرا شد.
دومرتبه روی دکمه کلیک می‌کنم:

درخواست دوباره مجوز بعد از یکبار رد کردن توسط کاربر

اینبار بجای نمایش دیالوگ مربوط به درخواست مجوز، AlertDialog اجرا می‌شود. اگر روی گزینه “موافقم” کلیک شود، درخواست مجوز مجدد انجام خواهد شد:

کاربرد متد shouldShowRequestPermissionRationale

البته اینبار یک تفاوت با گذشته وجود دارد. گزینه‌ای با عنوان Never ask again (یعنی دوباره سوال نکن) به درخواست اضافه شده. اگر کاربر این گزینه را تیک بزند و DENY را انتخاب کند با کلیک مجدد روی دکمه این درخواست اجرا نخواهد شد و بازهم پیغام “مجوز رد شد” اجرا می‌گردد. اگر در آینده شخص بخواهد این مجوز را تایید کند باید به صورت دستی و در تنظیمات برنامه (قسمت Permissions) آنرا فعال کند. در پروژه بعدی این عملیات یعنی انتقال از برنامه به صفحه تنظیمات را به صورت خودکار انجام می‌دهیم.

کد کامل MainActivity.java


package ir.android_studio.runtimepermission;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;

import android.Manifest;
import android.content.DialogInterface;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity {

    private final int CAMERA_REQUEST_CODE = 100;
    private Button requestButton;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        requestButton = findViewById(R.id.btn_request);

        requestButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {

                if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {

                    requestCameraPermission();

                } else {

                    Toast.makeText(MainActivity.this, "مجوز قبلا دریافت شده", Toast.LENGTH_SHORT).show();

                }

            }
        });
    }

    private void requestCameraPermission() {

        if (ActivityCompat.shouldShowRequestPermissionRationale(MainActivity.this, Manifest.permission.CAMERA)) {

            new AlertDialog.Builder(this)
                    .setTitle("درخواست مجوز")
                    .setMessage("برای دسترسی به دوربین باید مجوز را تایید کنید")
                    .setPositiveButton("موافقم", new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialogInterface, int i) {

                            reqPermission();

                        }
                    })
                    .setNegativeButton("لغو", new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialogInterface, int i) {

                            dialogInterface.dismiss();

                        }
                    })
                    .create()
                    .show();

        } else {

            reqPermission();

        }

    }

    private void reqPermission() {

        ActivityCompat.requestPermissions(MainActivity.this, new String[] {Manifest.permission.CAMERA}, CAMERA_REQUEST_CODE);

    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {

        if (requestCode == CAMERA_REQUEST_CODE) {

            if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {

                Toast.makeText(this, "مجوز تایید شد", Toast.LENGTH_SHORT).show();

            } else {

                Toast.makeText(this, "مجوز رد شد", Toast.LENGTH_SHORT).show();

            }

        }

    }
}

پروژه شماره ۲: درخواست چند مجوز همزمان در Runtime Permission

در پروژه قبل فقط درخواست تایید یک مجوز را از کاربر گرفتیم. در این پروژه قصد داریم همزمان تاییدیه سه مجوز را دریافت کنیم. سپس گزینه Never ask Again را طوری مدیریت کنیم که در صورت انتخاب آن توسط کاربر، در دفعات بعدی درخواست، کاربر مستقیم به صفحه تنظیمات برنامه هدایت شود تا بدین ترتیب از سردرگمی کاربر در عملکرد برنامه جلوگیری کنیم.
یک پروژه جدید با نام Multiple Runtime Permission در اندروید استودیو ایجاد می‌کنم.
ابتدا مجوزهای مدنظرم را در مانیفست پروژه تعریف می‌کنم:

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="ir.android_studio.multipleruntimepermission">

    <uses-permission android:name="android.permission.CAMERA"/>
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

مجوزها به ترتیب: دسترسی به دوربین، خواندن از کارت حافظه و نوشتن روی کارت حافظه.
در مرحله اول اکتیویتی را به صورت زیر تکمیل کرده‌ام:

MainActivity.java

package ir.android_studio.multipleruntimepermission;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;

import android.Manifest;
import android.content.DialogInterface;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity {

    private final int PERMISSION_REQUEST_CODE = 100;
    private Button requestButton;
    String[] requiredPermissions = new String[] {Manifest.permission.CAMERA, Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE};

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        requestButton = findViewById(R.id.btn_request);

        requestButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {

                if (ContextCompat.checkSelfPermission(MainActivity.this, requiredPermissions[0]) != PackageManager.PERMISSION_GRANTED
                 || ContextCompat.checkSelfPermission(MainActivity.this, requiredPermissions[1]) != PackageManager.PERMISSION_GRANTED
                 || ContextCompat.checkSelfPermission(MainActivity.this, requiredPermissions[2]) != PackageManager.PERMISSION_GRANTED) {

                    requestAppPermissions();

                } else {

                    Toast.makeText(MainActivity.this, "مجوز قبلا دریافت شده", Toast.LENGTH_SHORT).show();

                }

            }
        });

    }

    private void requestAppPermissions() {

        if (ActivityCompat.shouldShowRequestPermissionRationale(MainActivity.this, requiredPermissions[0])
                || ActivityCompat.shouldShowRequestPermissionRationale(MainActivity.this, requiredPermissions[1])
                || ActivityCompat.shouldShowRequestPermissionRationale(MainActivity.this, requiredPermissions[2])) {

            new AlertDialog.Builder(MainActivity.this)
                    .setTitle("درخواست مجوز")
                    .setMessage("برای دسترسی به دوربین و کارت حافظه باید مجوز را تایید کنید")
                    .setPositiveButton("موافقم", new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialogInterface, int i) {

                            reqPermissions();

                        }
                    })
                    .setNegativeButton("لغو", new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialogInterface, int i) {

                            dialogInterface.dismiss();

                        }
                    })
                    .create()
                    .show();

        } else {

            reqPermissions();

        }

    }

    private void reqPermissions() {

        ActivityCompat.requestPermissions(MainActivity.this, requiredPermissions, PERMISSION_REQUEST_CODE);

    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);

        if (requestCode == PERMISSION_REQUEST_CODE) {

            boolean allPermissionsGranted = false;

            for (int i = 0; i < grantResults.length; i++) {

                if (grantResults[i] == PackageManager.PERMISSION_GRANTED) {

                    allPermissionsGranted = true;

                } else {

                    allPermissionsGranted = false;
                    break;

                }

            }

            if (allPermissionsGranted) {

                Toast.makeText(MainActivity.this, "مجوزها تایید شدند", Toast.LENGTH_SHORT).show();

            } else {

                Toast.makeText(this, "مجوزها تایید نشدند", Toast.LENGTH_SHORT).show();

            }

        }

    }
}

ابتدا درون بدنه اکتیویتی یک متغیر از جنس int با مقدار دلخواه ۱۰۰ تعریف کردم که مانند پروژه قبل برای بررسی نتیجه درخواست مجوز استفاده می‌شود. سپس برای تعریف مجوزهای مدنظر از جنس String[] متغیری با نام requiredPermissions نوشتم که ۳ مجوز در آن تعریف شده است.
سپس داخل متد onCreate اکتیویتی یک setOnClickListener برای دکمه تعریف شده که با استفاده از متد checkSelfPermission شرطی درون آن تعریف شده. ما می‌خواهیم ۳ شرط بطور همزمان بررسی شود بنابراین متد checkSelfPermission نیز باید برای هر مجوز جداگانه نوشته شود:

if (ContextCompat.checkSelfPermission(MainActivity.this, requiredPermissions[0]) != PackageManager.PERMISSION_GRANTED
 || ContextCompat.checkSelfPermission(MainActivity.this, requiredPermissions[1]) != PackageManager.PERMISSION_GRANTED
 || ContextCompat.checkSelfPermission(MainActivity.this, requiredPermissions[2]) != PackageManager.PERMISSION_GRANTED)

واضح است که requiredPermissions با اندیس ۰ اولین مجوز تعریف شده یعنی CAMERA را برمی‌گرداند و به همین ترتیب برای دو مورد دیگر. در اینجا ما گفتیم اگر مجوز اول یا مجوز دوم یا مجوز سوم قبلا تایید نشده، متد requestAppPermissions را اجرا کن در غیر اینصورت پیغام “مجوز قبلا دریافت شده” را نمایش بده.
روی requestAppPermissions کلیدهای ترکیبی alt + enter زده و متد را به اکتیویتی اضافه کرده‌ام. سپس درون آن متد shouldShowRequestPermissionRationale را برای هرکدام از مجوزها فراخوانی کرده و تعیین کردم در صورتی که هرکدام از این شروط برقرار بود (یعنی حداقل یکی از مجوزها نیاز به نمایش توضیحات دارد) یک AlertDialog نمایش بده که با کلیک روی دکمه “موافقم” متد reqPermissions اجرا شود در غیر اینصورت مستقیما reqPermissions اجرا گردد:

private void reqPermissions() {

    ActivityCompat.requestPermissions(MainActivity.this, requiredPermissions, PERMISSION_REQUEST_CODE);

}

در اینجا و توسط متد requestPermissions مجوزهایی که قبلا در requiredPermissions تعریف کردیم از کاربر درخواست می‌شوند.
در ادامه لازم است متد onRequestPermissionsResult را مانند پروژه گذشته به اکتیویتی اضافه و تکمیل کنم:

@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults);

    if (requestCode == PERMISSION_REQUEST_CODE) {

        boolean allPermissionsGranted = false;

        for (int i = 0; i < grantResults.length; i++) {

            if (grantResults[i] == PackageManager.PERMISSION_GRANTED) {

                allPermissionsGranted = true;

            } else {

                allPermissionsGranted = false;
                break;

            }

        }

        if (allPermissionsGranted) {

            Toast.makeText(MainActivity.this, "مجوزها تایید شدند", Toast.LENGTH_SHORT).show();

        } else {

            Toast.makeText(this, " مجوزها تایید نشدند ", Toast.LENGTH_SHORT).show();

        }

    }

}

ابتدا کد درخواست و کد نتیجه دریافتی بررسی می‌شود. درون این شرط ابتدا یک boolean با نام allPermissionsGranted و مقدار اولیه false تعریف کردم. سپس یک حلقه for تعریف شده که از اولین تا آخرین نتیجه درخواست مجوز را بررسی می‌کند و چنانچه همه موارد توسط کاربر تایید شده باشد (PERMISSIONS_GRANTED) مقدار allPermissionsGranted برابر با true می‌شود در غیر اینصورت همان false باقی می‌ماند.
حالا یک if تعریف می‌کنم که اگر allPermissionsGranted برقرار باشد (یعنی true باشد) پیغام “مجوزها تایید شدند” نمایش داده شود و در غیر اینصورت پیغام “مجوزها تایید نشدند” ظاهر خواهد شد.
پروژه را اجرا می‌کنم:

درخواست مجوزهای چندگانه به صورت همزمان در Runtime Permission

درخواست مجوزهای دسترسی به دوربین و کارت حافظه از کاربر

مدیریت نتیجه متد checkSelfPermission

بعد از کلیک روی دکمه، دیالوگ مجوزها نمایش داده می‌شود. ابتدا تایید دسترسی به دوربین درخواست شده که آنرا تایید می‌کنم. سپس دسترسی به کارت حافظه درخواست می‌شود. آنرا نیز تایید می‌کنم. در نهایت پیغام “مجوزها تایید شدند” اجرا ‌شده است.

نکته: مجوزهایی که در یک گروه قرار دارند فقط یکبار تاییدیه آنها دریافت می‌شود. این گروه‌ها Permission Groups نام دارند. در این پروژه ما ۳ مجوز درخواست کردیم اما فقط دو درخواست برای کاربر صادر شده. مورد اول مربوط به دوربین و مورد دوم مربوط به دسترسی به کارت حافظه. به این دلیل که موارد دوم و سوم (خواندن از کارت حافظه و نوشتن روی کارت حافظه) هردو در گروه دسترسی به کارت حافظه قرار می‌گیرند. بنابراین اندروید برای سادگی کار و جلوگیری پیچیدگی روند دریافت تاییدیه، مجوزهایی که در یک دسته قرار می‌گیرند را ادغام کرده و فقط یک درخواست صادر می‌کند. در واقع وقتی کاربر دسترسی به مجوز دوم یعنی خواندن از کارت حافظه را تایید کرد، اندروید مجوز سوم (نوشتن روی کارت حافظه) را به صورت خودکار به اپلیکیشن اعطا می‌کند. برای سایر مجوزهای گروهی مانند پیامک (READ_SMS و RECEIVE_SMS) و دسترسی به مخاطبین (READ_CONTACTS و WRITE_CONTACTS) نیز به همین صورت انجام می‌شود.

بعد از تایید مجوزها دوباره روی دکمه کلیک می‌کنم:

بررسی مجوزهای تایید شده توسط grantResults

اینبار پیغام “مجوز قبلا دریافت شده” اجرا می‌شود. بنابراین عملیاتی که بعد از دریافت مجوزها باید انجام شود (مانند باز شدن دوربین و ذخیره عکس روی کارت حافظه) باید در قسمتی که این Toast تعریف شده قرار گیرد.
سپس حالت دوم را بررسی می‌کنیم. یعنی زمانی که کاربر مجوزها را تایید نکند.
ابتدا اپلیکیشن این پروژه را از روی دیوایس را حذف کرده و مجدد پروژه را اجرا می‌کنم. روی دکمه کلیک کرده و یک و یا هردو مجوز را DENY می‌کنم. برای مرتبه دوم روی Button کلیک می‌کنم:

نمایش توضیحاتی در خصوص ضرورت تایید مجوزها برای عملکرد اپلیکیشن

به دلیل اینکه قبلا یکبار درخواست مجوزها تایید نشده ابتدا AlertDialog اجرا شده که با تایید آن مجددا دیالوگ درخواست مجوزها اجرا می‌شود:

انتخاب گزینه Never ask again در ران تام پرمیشن

عدم درخواست دوباره مجوز بعد از انتخاب گزینه Never ask again توسط کاربر

اینبار هم برای یکی از دو مورد یا هردو، گزینه Never ask again را انتخاب و DENY می‌کنم. حالا در صورتی که کاربر برای مرتبه سوم روی دکمه کلیک کند، هیچ اتفاقی رخ نخواهد داد زیرا قبلا تایید کرده که مجوزها هیچگاه درخواست نشوند. اگر شخص این گزینه را از روی نا آگاهی انتخاب کرده و یا قبلا مایل به اعطای مجوز به برنامه نبوده و حالا تصمیمش عوض شده، در اینجا اگر آماتور باشد احتمال زیاد دچار سردرگمی خواهد شد. راه حل مناسب این است که در این مرحله او را به صورت خودکار به صفحه تنظیمات برنامه هدایت کنیم.
کد کامل اکتیویتی را در ادامه قرار داده و سپس به بررسی کدهای جدید می‌پردازم:

MainActivity.java

package ir.android_studio.multipleruntimepermission;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;

import android.Manifest;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
import android.provider.Settings;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity {

    private final int PERMISSION_REQUEST_CODE = 100;
    private static final int REQUEST_PERMISSION_SETTINGS = 101;
    private Button requestButton;
    String[] requiredPermissions = new String[] {Manifest.permission.CAMERA, Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE};
    SharedPreferences permissionStatus;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        permissionStatus = getSharedPreferences("permsStatus", MODE_PRIVATE);

        requestButton = findViewById(R.id.btn_request);

        requestButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {

                if (ContextCompat.checkSelfPermission(MainActivity.this, requiredPermissions[0]) != PackageManager.PERMISSION_GRANTED
                 || ContextCompat.checkSelfPermission(MainActivity.this, requiredPermissions[1]) != PackageManager.PERMISSION_GRANTED
                 || ContextCompat.checkSelfPermission(MainActivity.this, requiredPermissions[2]) != PackageManager.PERMISSION_GRANTED) {

                    requestAppPermissions();

                } else {

                    Toast.makeText(MainActivity.this, "مجوز قبلا دریافت شده", Toast.LENGTH_SHORT).show();

                }
            }
        });

    }

    private void requestAppPermissions() {

        if (ActivityCompat.shouldShowRequestPermissionRationale(MainActivity.this, requiredPermissions[0])
                || ActivityCompat.shouldShowRequestPermissionRationale(MainActivity.this, requiredPermissions[1])
                || ActivityCompat.shouldShowRequestPermissionRationale(MainActivity.this, requiredPermissions[2])) {

            new AlertDialog.Builder(MainActivity.this)
                    .setTitle("درخواست مجوز")
                    .setMessage("برای دسترسی به دوربین و کارت حافظه باید مجوز را تایید کنید")
                    .setPositiveButton("موافقم", new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialogInterface, int i) {

                            reqPermissions();

                        }
                    })
                    .setNegativeButton("لغو", new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialogInterface, int i) {

                            dialogInterface.dismiss();

                        }
                    })
                    .create()
                    .show();

        } else if (permissionStatus.getBoolean(requiredPermissions[0], false)) {

            new AlertDialog.Builder(MainActivity.this)
                    .setTitle("تایید دستی مجوزها")
                    .setMessage("لطفا در تنظیمات برنامه (گزینه Permissions) مجوزها را فعال کنید")
                    .setPositiveButton("موافقم", new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialogInterface, int i) {
                            dialogInterface.cancel();
                            Intent mIntent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
                            Uri packageUri = Uri.fromParts("package", getPackageName(), null);
                            mIntent.setData(packageUri);
                            startActivityForResult(mIntent, REQUEST_PERMISSION_SETTINGS);
                        }
                    })
                    .setNegativeButton("لغو", new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialogInterface, int i) {
                            dialogInterface.cancel();
                        }
                    })
                    .create()
                    .show();

        } else {

            reqPermissions();

        }

        SharedPreferences.Editor shEditor = permissionStatus.edit();
        shEditor.putBoolean(requiredPermissions[0], true);
        shEditor.apply();

    }

    private void reqPermissions() {

        ActivityCompat.requestPermissions(MainActivity.this, requiredPermissions, PERMISSION_REQUEST_CODE);

    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);

        if (requestCode == PERMISSION_REQUEST_CODE) {

            boolean allPermissionsGranted = false;

            for (int i = 0; i < grantResults.length; i++) {

                if (grantResults[i] == PackageManager.PERMISSION_GRANTED) {

                    allPermissionsGranted = true;

                } else {

                    allPermissionsGranted = false;
                    break;

                }

            }

            if (allPermissionsGranted) {

                Toast.makeText(MainActivity.this, "مجوزها تایید شدند", Toast.LENGTH_SHORT).show();

            } else {

                Toast.makeText(this, "مجوزها تایید نشدند", Toast.LENGTH_SHORT).show();

            }

        }

    }
}

ابتدا یک متغیر با نام REQUEST_PERMISSION_SETTINGS از جنس int و با مقدار دلخوه ۱۰۱ و همچنین یک شیء از SharedPreferences با نام permissionStatus تعریف کردم. قبلا در آموزش ذخیره اطلاعات با SharedPreferences در اندروید با این متد آشنا شدیم. از این متد برای ذخیره وضعیت مجوزها استفاده می‌کنیم. برای دسترسی به آن، خط زیر را در onCreate تعریف کردم:

permissionStatus = getSharedPreferences("permsStatus", MODE_PRIVATE);

در ادامه در شرط تعریف شده در متد requestAppPermissions یک else if اضافه شده:

else if (permissionStatus.getBoolean(requiredPermissions[0], false)) {

    new AlertDialog.Builder(MainActivity.this)
            .setTitle("تایید دستی مجوزها")
            .setMessage("لطفا در تنظیمات برنامه (گزینه Permissions) مجوزها را فعال کنید ")
            .setPositiveButton("موافقم", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialogInterface, int i) {
                    dialogInterface.cancel();
                    Intent mIntent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
                    Uri packageUri = Uri.fromParts("package", getPackageName(), null);
                    mIntent.setData(packageUri);
                    startActivityForResult(mIntent, REQUEST_PERMISSION_SETTINGS);
                }
            })
            .setNegativeButton("لغو", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialogInterface, int i) {
                    dialogInterface.cancel();
                }
            })
            .create()
            .show();

}

به خط زیر توجه کنید:

permissionStatus.getBoolean(requiredPermissions[0], false)

در اینجا گفتیم اگر حداقل یکی از درخواست مجوزها تایید نشد (یعنی getBoolean مقدار false برگرداند) شرط را اجرا کن. در اینجا هم یک AlertDialog داریم که به کاربر توضیح می‌دهد باید به صورت دستی مجوزها را فعال کند. در صورت تایید پیغام، دستورات زیر اجرا خواهد شد:

Intent mIntent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
Uri packageUri = Uri.fromParts("package", getPackageName(), null);
mIntent.setData(packageUri);
startActivityForResult(mIntent, REQUEST_PERMISSION_SETTINGS);

کاری که انجام دادیم این است که با استفاده از Intent کاربر را به صفحه تنظیمات برنامه هدایت می‌کنیم. برای اینکه مشخص شود کاربر باید به صفحه تنظیمات چه برنامه‌ای هدایت شود از Uri استفاده شده که توسط Uri.fromParts و پارامتر getPackageName این کار انجام می‌پذیرد. همانطور که از نام getPackageName پیداست وظیفه آن برگرداندن نام پکیج برنامه است.
اینکه نتیجه دریافت مجوز true هست یا false در کد زیر در SharedPreferences قبل از اجرای شرط ذخیره شده است:

SharedPreferences.Editor shEditor = permissionStatus.edit();
shEditor.putBoolean(requiredPermissions[0], true);
shEditor.apply();

در ابتدا مقدار پیش فرض true برای وضعیت دریافت مجوز ثبت می‌شود اما چنانچه یک مورد در وضعیت false قرار گرفته باشد (یعنی گزینه Never ask again انتخاب شده باشد)، مقدار false توسط putBoolean به shEditor فرستاده می‌شود.
پروژه را اجرا می‌کنم. من قبلا گزینه Never ask again را انتخاب کرده بودم بنابراین اگر کد من ایرادی نداشته باشد باید AlertDialog مربوط به مجوز دستی اجرا شود:

مدیریت گزینه Never ask again هنگام درخواست جواز از کاربر

هدایت کاربر به صفحه App Settings توسط intent

فعال کردن مجوزها در صفحه تنظیمات برنامه به صورت دستی توسط کاربر

هدف ما با موفقیت انجام شد.

نکته: دریافت یکباره تمامی مجوزها و در هنگام اولین اجرای برنامه توصیه نمی‌شود. بهتر است هر دسترسی فقط هنگامی که کاربر به آن نیاز دارد گرفته شود تا اعتماد بیشتری به امنیت برنامه داشته باشد. برای مثال دسترسی به دوربین زمانی گرفته شود که شخص قصد دارد از طریق برنامه شما تصویری را ثبت کند و اینکار را با آگاهی انجام دهد. دریافت یکباره مجوزهای متعدد ناخودآگاه حس منفی را به کاربر القا خواهد کرد.

اگر معتقدید که این حجم از کد برای دریافت یک مجوز ساده آزار دهنده است باید بگویم که شما تنها نفری نیستید که چنین عقیده‌ای دارید! برای همین توسعه دهندگان زیادی دست به کار شده و کتابخانه‌های مختلفی را برای ساده‌تر کردن روند دریافت تاییدیه مجوز یا همان Runtime Permission منتشر کرده‌اند. معروفترین کتابخانه‌ها عبارت اند از: Dexter، Android-Permissions، EasyPermissions و Let.
کار با این کتابخانه‌ها بسیار ساده است. با اینحال در صورتی که فرصتی فراهم شود آموزشی پیرامون معتبرترین کتابخانه یعنی Dexter تهیه خواهم کرد.
امیدوارم در این آموزش به اکثریت نکات مربوط به قابلیت Runtime Permission پرداخته باشم و نکته مبهمی برای شما عزیزان باقی نمانده باشد. چنانچه سوالی به ذهنتان خطور کرد در دیدگاه‌ها مطرح کنید تا در حد دانش خودم راهنمایی کنم.
موفق و پیروز باشید.

مطالعه‌ی بیشتر:

https://developer.android.com/guide/topics/permissions/overview
https://developer.android.com/training/permissions/requesting.html
https://developer.android.com/reference/android/Manifest.permission.html

توجه : سورس پروژه‌ها درون پوشه Exercises قرار دارد

دانلود نسخه PDF این آموزش به همراه سورس پروژه‌ها
تعداد صفحات : ۴۵
حجم : ۳ مگابایت
قیمت : رایگان
دانلود رایگان با حجم ۳ مگابایت لینک کمکی
این مطلب چقدر برایتان مفید بود؟ لطفا امتیاز دهید
4.6/5 - (17 امتیاز)
پرسش‌ها و دیدگاه‌های کاربران
دوره آموزش برنامه نویسی اندروید
دوره آموزش برنامه نویسی اندروید

با دریافت این دوره به تمامی آموزش‌های غیر رایگان و رایگان موجود در وب سایت دسترسی دارید که تخفیفی برای آموزش‌های غیر رایگان نیز درنظر گرفته شده. این پکیج به دو صورت دانلودی و ارسال پستی ارائه می‌گردد.
آموزش‌های اندروید استودیو در دو دسته «پایه» و «تکمیلی» منتشر می‌شوند.
آموزش‌های پایه شامل مباحث اصلی و ضروری و آموزش‌های تکمیلی مطالبی است که می‌بایست در کنار مطالب اصلی بررسی شود.
با خرید این دوره، به تمامی آموزش‌های غیر رایگانی که در آینده منتشر می‌شود نیز به صورت رایگان دسترسی خواهید داشت!

یک دیدگاه بنویسید

پرسش‌های زیر تایید و پاسخ داده نـــخواهند شد:
۱: جزء موارد مطرح شده در صفحات مشکلات و پرسش‌های رایج و بروزرسانی‌های محتوای آموزشی باشد
۲: سوال قبلا توسط کاربران در دیدگاه‌ها مطرح و پاسخ داده شده باشد
۳: پرسش خارج از مبحث آموزشی موجود در این صفحه باشد