簡體   English   中英

應用程序在單擊以開始位置更新時崩潰

[英]App Crashes On Clicking To Start Location Update

我正在開發一個應用程序,該應用程序使用指紋身份驗證以30分鍾的間隔(每隔10分鍾)發送用戶在一定時間內的位置。 我已經使用某些代碼來開發應用程序。 但是,在單擊“開始更新”按鈕時應用程序崩潰。

注意:應用程序崩潰的摘要顯示為java.lang.SecurityException:客戶端必須具有ACCESS_Fine_LOCATION權限才能請求PRIORITY_HIGH_ACCURACY位置。

以下是代碼

表現

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.google.android.gms.location.sample.locationupdates">

<uses-permission android:name="android.permission.USE_FINGERPRINT" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-feature android:name="android.hardware.location.gps" />

<application
    android:allowBackup="true"
    android:icon="@drawable/ic_launcher"
    android:label="@string/app_name"
    android:theme="@style/AppTheme.NoActionBar">
    <meta-data
        android:name="com.google.android.gms.version"
        android:value="@integer/google_play_services_version" />

    <activity
        android:name=".FingerprintActivity"
        android:label="Fingerprint">
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />

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

</manifest>

FingerprintActivity.java

import android.Manifest; 
import android.annotation.TargetApi;
import android.app.KeyguardManager;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.hardware.fingerprint.FingerprintManager; 
import android.os.Build;
import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyPermanentlyInvalidatedException;
import android.security.keystore.KeyProperties;
import android.support.v4.app.ActivityCompat;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;
import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;

public class FingerprintActivity extends AppCompatActivity {

private KeyStore keyStore;
// Variable used for storing the key in the Android Keystore container
private static final String KEY_NAME = "androidHive";
private Cipher cipher;
private TextView textView;
public void sendMessage(View view)
{
    Intent intent = new Intent(FingerprintActivity.this, MainActivity.class);
    startActivity(intent);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_fingerprint);

    // Initializing both Android Keyguard Manager and Fingerprint Manager
    KeyguardManager keyguardManager = (KeyguardManager) getSystemService(KEYGUARD_SERVICE);
    FingerprintManager fingerprintManager = (FingerprintManager) getSystemService(FINGERPRINT_SERVICE);

    textView = (TextView) findViewById(R.id.errorText);

    // Check whether the device has a Fingerprint sensor.
    if(!fingerprintManager.isHardwareDetected()){
        /**
         * An error message will be displayed if the device does not contain the fingerprint hardware.
         * However if you plan to implement a default authentication method,
         * you can redirect the user to a default authentication activity from here.
         * Example:
         * Intent intent = new Intent(this, DefaultAuthenticationActivity.class);
         * startActivity(intent);
         */
        textView.setText("Your Device does not have a Fingerprint Sensor");
    }else {
        // Checks whether fingerprint permission is set on manifest
        if (ActivityCompat.checkSelfPermission(this, Manifest.permission.USE_FINGERPRINT) != PackageManager.PERMISSION_GRANTED) {
            textView.setText("Fingerprint authentication permission not enabled");
        }else{
            // Check whether at least one fingerprint is registered
            if (!fingerprintManager.hasEnrolledFingerprints()) {
                textView.setText("Register at least one fingerprint in Settings");
            }else{
                // Checks whether lock screen security is enabled or not
                if (!keyguardManager.isKeyguardSecure()) {
                    textView.setText("Lock screen security not enabled in Settings");
                }else{
                    generateKey();

                    if (cipherInit()) {
                        FingerprintManager.CryptoObject cryptoObject = new FingerprintManager.CryptoObject(cipher);
                        FingerprintHandler helper = new FingerprintHandler(this);
                        helper.startAuth(fingerprintManager, cryptoObject);
                    }
                }
            }
        }
    }
}

@TargetApi(Build.VERSION_CODES.M)
protected void generateKey() {
    try {
        keyStore = KeyStore.getInstance("AndroidKeyStore");
    } catch (Exception e) {
        e.printStackTrace();
    }

    KeyGenerator keyGenerator;
    try {
        keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore");
    } catch (NoSuchAlgorithmException | NoSuchProviderException e) {
        throw new RuntimeException("Failed to get KeyGenerator instance", e);
    }

    try {
        keyStore.load(null);
        keyGenerator.init(new
                KeyGenParameterSpec.Builder(KEY_NAME,
                KeyProperties.PURPOSE_ENCRYPT |
                        KeyProperties.PURPOSE_DECRYPT)
                .setBlockModes(KeyProperties.BLOCK_MODE_CBC)
                .setUserAuthenticationRequired(true)
                .setEncryptionPaddings(
                        KeyProperties.ENCRYPTION_PADDING_PKCS7)
                .build());
        keyGenerator.generateKey();
    } catch (NoSuchAlgorithmException |
            InvalidAlgorithmParameterException
            | CertificateException | IOException e) {
        throw new RuntimeException(e);
    }
}

@TargetApi(Build.VERSION_CODES.M)
public boolean cipherInit() {
    try {
        cipher = Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/" + KeyProperties.BLOCK_MODE_CBC + "/" + KeyProperties.ENCRYPTION_PADDING_PKCS7);
    } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
        throw new RuntimeException("Failed to get Cipher", e);
    }

    try {
        keyStore.load(null);
        SecretKey key = (SecretKey) keyStore.getKey(KEY_NAME,
                null);
        cipher.init(Cipher.ENCRYPT_MODE, key);
        return true;
    } catch (KeyPermanentlyInvalidatedException e) {
        return false;
    } catch (KeyStoreException | CertificateException | UnrecoverableKeyException | IOException | NoSuchAlgorithmException | InvalidKeyException e) {
        throw new RuntimeException("Failed to init Cipher", e);
    }
}
}

FingerprintHandler

import android.Manifest;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.hardware.fingerprint.FingerprintManager;
import android.os.CancellationSignal;
import android.support.v4.app.ActivityCompat;
import android.widget.TextView;

public class FingerprintHandler extends FingerprintManager.AuthenticationCallback {

private Context context;

// Constructor
public FingerprintHandler(Context mContext) {
    context = mContext;
}

public void startAuth(FingerprintManager manager, FingerprintManager.CryptoObject cryptoObject) {
    CancellationSignal cancellationSignal = new CancellationSignal();
    if (ActivityCompat.checkSelfPermission(context, Manifest.permission.USE_FINGERPRINT) != PackageManager.PERMISSION_GRANTED) {
        return;
    }
    manager.authenticate(cryptoObject, cancellationSignal, 0, this, null);
}

@Override
public void onAuthenticationError(int errMsgId, CharSequence errString) {
    this.update("Fingerprint Authentication error\n" + errString);
}

@Override
public void onAuthenticationHelp(int helpMsgId, CharSequence helpString) {
    this.update("Fingerprint Authentication help\n" + helpString);
}

@Override
public void onAuthenticationFailed() {
    this.update("Fingerprint Authentication failed.");
}

@Override
public void onAuthenticationSucceeded(FingerprintManager.AuthenticationResult result) {
    ((Activity) context).finish();
    Intent intent = new Intent(context, MainActivity.class);
    context.startActivity(intent);
}

private void update(String e){
    TextView textView = (TextView) ((Activity)context).findViewById(R.id.errorText);
    textView.setText(e);
}

}

主要活動

import android.app.Activity;
import android.content.Intent;
import android.content.IntentSender;
import android.location.Location;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;

import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.api.GoogleApiClient;
import com.google.android.gms.common.api.GoogleApiClient.ConnectionCallbacks;
import com.google.android.gms.common.api.GoogleApiClient.OnConnectionFailedListener;
import com.google.android.gms.common.api.ResultCallback;
import com.google.android.gms.common.api.Status;
import com.google.android.gms.location.LocationListener;
import com.google.android.gms.location.LocationRequest;
import com.google.android.gms.location.LocationServices;
import com.google.android.gms.location.LocationSettingsRequest;
import com.google.android.gms.location.LocationSettingsResult;
import com.google.android.gms.location.LocationSettingsStatusCodes;

import java.text.DateFormat;
import java.util.Date;

public class MainActivity extends AppCompatActivity implements
    ConnectionCallbacks,
    OnConnectionFailedListener,
    LocationListener {

protected static final String TAG = "MainActivity";

/**
 * Constant used in the location settings dialog.
 */
protected static final int REQUEST_CHECK_SETTINGS = 0x1;


public static final long UPDATE_INTERVAL_IN_MILLISECONDS = 10000;


public static final long FASTEST_UPDATE_INTERVAL_IN_MILLISECONDS =
        UPDATE_INTERVAL_IN_MILLISECONDS / 2;

// Keys for storing activity state in the Bundle.
protected final static String KEY_REQUESTING_LOCATION_UPDATES = "requesting-location-updates";
protected final static String KEY_LOCATION = "location";
protected final static String KEY_LAST_UPDATED_TIME_STRING = "last-updated-time-string";


protected GoogleApiClient mGoogleApiClient;

protected LocationRequest mLocationRequest;


protected LocationSettingsRequest mLocationSettingsRequest;

protected Location mCurrentLocation;

// UI Widgets.
protected Button mStartUpdatesButton;
protected Button mStopUpdatesButton;
protected TextView mLastUpdateTimeTextView;
protected TextView mLatitudeTextView;
protected TextView mLongitudeTextView;
protected TextView mLocationInadequateWarning;

// Labels.
protected String mLatitudeLabel;
protected String mLongitudeLabel;
protected String mLastUpdateTimeLabel;


protected Boolean mRequestingLocationUpdates;


protected String mLastUpdateTime;

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main_activity);
    Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
    setSupportActionBar(toolbar);


    // Locate the UI widgets.
    mStartUpdatesButton = (Button) findViewById(R.id.start_updates_button);
    mStopUpdatesButton = (Button) findViewById(R.id.stop_updates_button);
    mLatitudeTextView = (TextView) findViewById(R.id.latitude_text);
    mLongitudeTextView = (TextView) findViewById(R.id.longitude_text);
    mLastUpdateTimeTextView = (TextView) findViewById(R.id.last_update_time_text);
    mLocationInadequateWarning = (TextView) findViewById(R.id.location_inadequate_warning);

    // Set labels.
    mLatitudeLabel = getResources().getString(R.string.latitude_label);
    mLongitudeLabel = getResources().getString(R.string.longitude_label);
    mLastUpdateTimeLabel = getResources().getString(R.string.last_update_time_label);

    mRequestingLocationUpdates = false;
    mLastUpdateTime = "";

    // Update values using data stored in the Bundle.
    updateValuesFromBundle(savedInstanceState);

    // Kick off the process of building the GoogleApiClient, LocationRequest, and
    // LocationSettingsRequest objects.
    buildGoogleApiClient();
    createLocationRequest();
    buildLocationSettingsRequest();
}


private void updateValuesFromBundle(Bundle savedInstanceState) {
    if (savedInstanceState != null) {
        // Update the value of mRequestingLocationUpdates from the Bundle, and make sure that
        // the Start Updates and Stop Updates buttons are correctly enabled or disabled.
        if (savedInstanceState.keySet().contains(KEY_REQUESTING_LOCATION_UPDATES)) {
            mRequestingLocationUpdates = savedInstanceState.getBoolean(
                    KEY_REQUESTING_LOCATION_UPDATES);
        }

        // Update the value of mCurrentLocation from the Bundle and update the UI to show the
        // correct latitude and longitude.
        if (savedInstanceState.keySet().contains(KEY_LOCATION)) {
            // Since KEY_LOCATION was found in the Bundle, we can be sure that mCurrentLocation
            // is not null.
            mCurrentLocation = savedInstanceState.getParcelable(KEY_LOCATION);
        }

        // Update the value of mLastUpdateTime from the Bundle and update the UI.
        if (savedInstanceState.keySet().contains(KEY_LAST_UPDATED_TIME_STRING)) {
            mLastUpdateTime = savedInstanceState.getString(KEY_LAST_UPDATED_TIME_STRING);
        }
        updateUI();
    }
}

/**
 * Builds a GoogleApiClient. Uses the {@code #addApi} method to request the
 * LocationServices API.
 */
protected synchronized void buildGoogleApiClient() {
    Log.i(TAG, "Building GoogleApiClient");
    mGoogleApiClient = new GoogleApiClient.Builder(this)
            .addConnectionCallbacks(this)
            .addOnConnectionFailedListener(this)
            .addApi(LocationServices.API)
            .build();
}


protected void createLocationRequest() {
    mLocationRequest = new LocationRequest();

    // Sets the desired interval for active location updates. This interval is
    // inexact. You may not receive updates at all if no location sources are available, or
    // you may receive them slower than requested. You may also receive updates faster than
    // requested if other applications are requesting location at a faster interval.
    mLocationRequest.setInterval(UPDATE_INTERVAL_IN_MILLISECONDS);

    // Sets the fastest rate for active location updates. This interval is exact, and your
    // application will never receive updates faster than this value.
    mLocationRequest.setFastestInterval(FASTEST_UPDATE_INTERVAL_IN_MILLISECONDS);

    mLocationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY);
}

/**
 * Uses a {@link com.google.android.gms.location.LocationSettingsRequest.Builder} to build
 * a {@link com.google.android.gms.location.LocationSettingsRequest} that is used for checking
 * if a device has the needed location settings.
 */
protected void buildLocationSettingsRequest() {
    LocationSettingsRequest.Builder builder = new LocationSettingsRequest.Builder();
    builder.addLocationRequest(mLocationRequest);
    mLocationSettingsRequest = builder.build();
}

/**
 * The callback invoked when
 * {@link com.google.android.gms.location.SettingsApi#checkLocationSettings(GoogleApiClient,
 * LocationSettingsRequest)} is called. Examines the
 * {@link com.google.android.gms.location.LocationSettingsResult} object and determines if
 * location settings are adequate. If they are not, begins the process of presenting a location
 * settings dialog to the user.
 */


@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    switch (requestCode) {
        // Check for the integer request code originally supplied to startResolutionForResult().
        case REQUEST_CHECK_SETTINGS:
            switch (resultCode) {
                case Activity.RESULT_OK:
                    Log.i(TAG, "User agreed to make required location settings changes.");
                    // Nothing to do. startLocationupdates() gets called in onResume again.
                    break;
                case Activity.RESULT_CANCELED:
                    Log.i(TAG, "User chose not to make required location settings changes.");
                    mRequestingLocationUpdates = false;
                    updateUI();
                    break;
            }
            break;
    }
}

/**
 * Handles the Start Updates button and requests start of location updates. Does nothing if
 * updates have already been requested.
 */
public void startUpdatesButtonHandler(View view) {
    if (!mRequestingLocationUpdates) {
        mRequestingLocationUpdates = true;
        setButtonsEnabledState();
        startLocationUpdates();
    }
}

/**
 * Handles the Stop Updates button, and requests removal of location updates.
 */
public void stopUpdatesButtonHandler(View view) {
    // It is a good practice to remove location requests when the activity is in a paused or
    // stopped state. Doing so helps battery performance and is especially
    // recommended in applications that request frequent location updates.
    stopLocationUpdates();
}

/**
 * Requests location updates from the FusedLocationApi.
 */
protected void startLocationUpdates() {
    LocationServices.SettingsApi.checkLocationSettings(
            mGoogleApiClient,
            mLocationSettingsRequest
    ).setResultCallback(new ResultCallback<LocationSettingsResult>() {
        @Override
        public void onResult(LocationSettingsResult locationSettingsResult) {
            final Status status = locationSettingsResult.getStatus();
            switch (status.getStatusCode()) {
                case LocationSettingsStatusCodes.SUCCESS:
                    Log.i(TAG, "All location settings are satisfied.");
                    LocationServices.FusedLocationApi.requestLocationUpdates(
                            mGoogleApiClient, mLocationRequest, MainActivity.this);
                    break;
                case LocationSettingsStatusCodes.RESOLUTION_REQUIRED:
                    Log.i(TAG, "Location settings are not satisfied. Attempting to upgrade " +
                            "location settings ");
                    try {
                        // Show the dialog by calling startResolutionForResult(), and check the
                        // result in onActivityResult().
                        status.startResolutionForResult(MainActivity.this, REQUEST_CHECK_SETTINGS);
                    } catch (IntentSender.SendIntentException e) {
                        Log.i(TAG, "PendingIntent unable to execute request.");
                    }
                    break;
                case LocationSettingsStatusCodes.SETTINGS_CHANGE_UNAVAILABLE:
                    String errorMessage = "Location settings are inadequate, and cannot be " +
                            "fixed here. Fix in Settings.";
                    Log.e(TAG, errorMessage);
                    Toast.makeText(MainActivity.this, errorMessage, Toast.LENGTH_LONG).show();
                    mRequestingLocationUpdates = false;
            }
            updateUI();
        }
    });

}

/**
 * Updates all UI fields.
 */
private void updateUI() {
    setButtonsEnabledState();
    updateLocationUI();
}

/**
 * Disables both buttons when functionality is disabled due to insuffucient location settings.
 * Otherwise ensures that only one button is enabled at any time. The Start Updates button is
 * enabled if the user is not requesting location updates. The Stop Updates button is enabled
 * if the user is requesting location updates.
 */
private void setButtonsEnabledState() {
    if (mRequestingLocationUpdates) {
        mStartUpdatesButton.setEnabled(false);
        mStopUpdatesButton.setEnabled(true);
    } else {
        mStartUpdatesButton.setEnabled(true);
        mStopUpdatesButton.setEnabled(false);
    }
}

/**
 * Sets the value of the UI fields for the location latitude, longitude and last update time.
 */
private void updateLocationUI() {
    if (mCurrentLocation != null) {
        mLatitudeTextView.setText(String.format("%s: %f", mLatitudeLabel,
                mCurrentLocation.getLatitude()));
        mLongitudeTextView.setText(String.format("%s: %f", mLongitudeLabel,
                mCurrentLocation.getLongitude()));
        mLastUpdateTimeTextView.setText(String.format("%s: %s", mLastUpdateTimeLabel,
                mLastUpdateTime));
    }
}

/**
 * Removes location updates from the FusedLocationApi.
 */
protected void stopLocationUpdates() {
    // It is a good practice to remove location requests when the activity is in a paused or
    // stopped state. Doing so helps battery performance and is especially
    // recommended in applications that request frequent location updates.
    LocationServices.FusedLocationApi.removeLocationUpdates(
            mGoogleApiClient,
            this
    ).setResultCallback(new ResultCallback<Status>() {
        @Override
        public void onResult(Status status) {
            mRequestingLocationUpdates = false;
            setButtonsEnabledState();
        }
    });
}

@Override
protected void onStart() {
    super.onStart();
    mGoogleApiClient.connect();
}

@Override
public void onResume() {
    super.onResume();
    // Within {@code onPause()}, we pause location updates, but leave the
    // connection to GoogleApiClient intact.  Here, we resume receiving
    // location updates if the user has requested them.
    if (mGoogleApiClient.isConnected() && mRequestingLocationUpdates) {
        startLocationUpdates();
    }
    updateUI();
}

@Override
protected void onPause() {
    super.onPause();
    // Stop location updates to save battery, but don't disconnect the GoogleApiClient object.
    if (mGoogleApiClient.isConnected()) {
        stopLocationUpdates();
    }
}

@Override
protected void onStop() {
    super.onStop();
    mGoogleApiClient.disconnect();
}

/**
 * Runs when a GoogleApiClient object successfully connects.
 */
@Override
public void onConnected(Bundle connectionHint) {
    // If the initial location was never previously requested, we use
    // FusedLocationApi.getLastLocation() to get it. If it was previously requested, we store
    // its value in the Bundle and check for it in onCreate(). We
    // do not request it again unless the user specifically requests location updates by pressing
    // the Start Updates button.
    //
    // Because we cache the value of the initial location in the Bundle, it means that if the
    // user launches the activity,
    // moves to a new location, and then changes the device orientation, the original location
    // is displayed as the activity is re-created.
    if (mCurrentLocation == null) {
        mCurrentLocation = LocationServices.FusedLocationApi.getLastLocation(mGoogleApiClient);
        mLastUpdateTime = DateFormat.getTimeInstance().format(new Date());
        updateLocationUI();
    }
    if (mRequestingLocationUpdates) {
        Log.i(TAG, "in onConnected(), starting location updates");
        startLocationUpdates();
    }

}

/**
 * Callback that fires when the location changes.
 */
@Override
public void onLocationChanged(Location location) {
    mCurrentLocation = location;
    mLastUpdateTime = DateFormat.getTimeInstance().format(new Date());
    updateLocationUI();
}

@Override
public void onConnectionSuspended(int cause) {
    Log.i(TAG, "Connection suspended");
}

@Override
public void onConnectionFailed(ConnectionResult result) {
    // Refer to the javadoc for ConnectionResult to see what error codes might be returned in
    // onConnectionFailed.
    Log.i(TAG, "Connection failed: ConnectionResult.getErrorCode() = " + result.getErrorCode());
}

/**
 * Stores activity data in the Bundle.
 */
public void onSaveInstanceState(Bundle savedInstanceState) {
    savedInstanceState.putBoolean(KEY_REQUESTING_LOCATION_UPDATES, mRequestingLocationUpdates);
    savedInstanceState.putParcelable(KEY_LOCATION, mCurrentLocation);
    savedInstanceState.putString(KEY_LAST_UPDATED_TIME_STRING, mLastUpdateTime);
    super.onSaveInstanceState(savedInstanceState);
}

@Override
public boolean onCreateOptionsMenu(Menu menu) {
    // Inflate the menu; this adds items to the action bar if it is present.
    getMenuInflater().inflate(R.menu.menu_main, menu);
    return true;
}

@Override
public boolean onOptionsItemSelected(MenuItem item) {
    // Handle action bar item clicks here. The action bar will
    // automatically handle clicks on the Home/Up button, so long
    // as you specify a parent activity in AndroidManifest.xml.
    int id = item.getItemId();

    if (id == R.id.action_settings) {
        return true;
    }

    return super.onOptionsItemSelected(item);
}
}

minSdkVersion 23目標SdkVersion 24

Targeting SDK 23+ 需要您支持運行時權限。 您的清單是<uses-permission>在這種情況下是無關緊要的(如果您不針對較低的SDK,可以將其刪除)。 由於您不支持運行時權限,因此您的應用未獲得GPS訪問權限,因此出現SecurityException崩潰。

文件: https//developer.android.com/training/permissions/requesting.html

快速解決方案:如果可以,請將targetSdk(和minSdk)降低到23以下。如果由於某種原因不能,則必須實現運行時權限。 有一些庫可以幫助完成此任務-僅使用google即可。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM