简体   繁体   中英

App Crashes On Clicking To Start Location Update

I am developing an app that uses fingerprint authentication to send the location of user over certain duration ex 30 mins in form of intervals of 10 mins. i have used certain code to develop the app. But app crashes on clicking start update button.

Note : Summary of app crash says java.lang.SecurityException: Client must have ACCESS_Fine_LOCATION permission to request PRIORITY_HIGH_ACCURACY locations.

Following is the code

Manifest

<?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);
}

}

MainActivity

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 targetSdkVersion 24

Targeting SDK 23+ require you support Runtime Permissions. Your manifest is <uses-permission> s are irrelevant in such case (and can be removed if you not targetting lower SDKs). As you have no support for Runtime Permissions your app is not granted GPS access hence the crash with SecurityException .

Docs: https://developer.android.com/training/permissions/requesting.html

Quick solution: if you can, lower targetSdk (and minSdk) below 23. If you for any reason cannot, you must have Runtime Permissions implemented. There are some libs helping with the task - just google for it.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM