Android 10.0 系统framework禁止访问应用信息页

本文详细介绍了在Android 10.0中,如何通过修改系统frameworks来禁止应用崩溃后弹窗的查看应用详情选项,主要涉及AppErrorDialog.java、AppErrors.java、ContextImpl.java和Activity.java四个关键代码部分,通过注释相关源码和判断Intent阻止ACTION_APPLICATION_DETAILS_SETTINGS的启动,达到禁止进入应用详情页的目的。
摘要由CSDN通过智能技术生成

目录

1.概述

2.系统frameworks禁止进入应用信息页的核心代码

3.系统frameworks禁止进入应用信息页的核心代码功能分析

  3.1AppErrorDialog.java关于错误信息的分析

3.2 AppErrors.java的相关代码

3.3 ContextImpl.java关于启动Activity的相关代码调用

3.4 Activity.java中关于禁止启动应用信息的相关代码


1.概述

   在定制化开发中,在app崩溃后,会弹出弹窗提醒崩溃了,然后弹窗可以通过点击应用信息按钮进入应用信息页这对于权限的管控不好处理,所以要求frameworks层禁止通过activity进入应用详情页

2.系统frameworks禁止进入应用信息页的核心代码

   frameworks/base/core/java/android/app/Activity.java
   frameworks/base/core/java/android/app/ContextImpl.java
   frameworks/base/services/core/java/com/android/server/am/AppErrorDialog.java
   frameworks/base/services/core/java/com/android/server/am/AppErrors.java

3.系统frameworks禁止进入应用信息页的核心代码功能分析

  3.1AppErrorDialog.java关于错误信息的分析

  final class AppErrorDialog extends BaseErrorDialog implements View.OnClickListener {

    private final ActivityManagerService mService;
    private final AppErrorResult mResult;
    private final ProcessRecord mProc;
    private final boolean mIsRestartable;

    static int CANT_SHOW = -1;
    static int BACKGROUND_USER = -2;
    static int ALREADY_SHOWING = -3;

    // Event 'what' codes
    static final int FORCE_QUIT = 1;
    static final int FORCE_QUIT_AND_REPORT = 2;
    static final int RESTART = 3;
    static final int MUTE = 5;
    static final int TIMEOUT = 6;
    static final int CANCEL = 7;
    static final int APP_INFO = 8;

    // 5-minute timeout, then we automatically dismiss the crash dialog
    static final long DISMISS_TIMEOUT = 1000 * 60 * 5;

    public AppErrorDialog(Context context, ActivityManagerService service, Data data) {
        super(context);
        Resources res = context.getResources();

        mService = service;
        mProc = data.proc;
        mResult = data.result;
        mIsRestartable = (data.taskId != INVALID_TASK_ID || data.isRestartableForService)
                && Settings.Global.getInt(context.getContentResolver(),
                Settings.Global.SHOW_RESTART_IN_CRASH_DIALOG, 0) != 0;
        BidiFormatter bidi = BidiFormatter.getInstance();

        CharSequence name;
        if ((mProc.pkgList.size() == 1) &&
                (name = context.getPackageManager().getApplicationLabel(mProc.info)) != null) {
            setTitle(res.getString(
                    data.repeating ? com.android.internal.R.string.aerr_application_repeated
                            : com.android.internal.R.string.aerr_application,
                    bidi.unicodeWrap(name.toString()),
                    bidi.unicodeWrap(mProc.info.processName)));
        } else {
            name = mProc.processName;
            setTitle(res.getString(
                    data.repeating ? com.android.internal.R.string.aerr_process_repeated
                            : com.android.internal.R.string.aerr_process,
                    bidi.unicodeWrap(name.toString())));
        }

        setCancelable(true);
        setCancelMessage(mHandler.obtainMessage(CANCEL));

        WindowManager.LayoutParams attrs = getWindow().getAttributes();
        attrs.setTitle("Application Error: " + mProc.info.processName);
        attrs.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_SYSTEM_ERROR
                | WindowManager.LayoutParams.PRIVATE_FLAG_SHOW_FOR_ALL_USERS;
        getWindow().setAttributes(attrs);
        if (mProc.isPersistent()) {
            getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ERROR);
        }

        // After the timeout, pretend the user clicked the quit button
        mHandler.sendMessageDelayed(
                mHandler.obtainMessage(TIMEOUT),
                DISMISS_TIMEOUT);
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        final FrameLayout frame = findViewById(android.R.id.custom);
        final Context context = getContext();
        LayoutInflater.from(context).inflate(
                com.android.internal.R.layout.app_error_dialog, frame, true);

        final boolean hasReceiver = mProc.errorReportReceiver != null;

        final TextView restart = findViewById(com.android.internal.R.id.aerr_restart);
        restart.setOnClickListener(this);
        restart.setVisibility(mIsRestartable ? View.VISIBLE : View.GONE);
        final TextView report = findViewById(com.android.internal.R.id.aerr_report);
        report.setOnClickListener(this);
        report.setVisibility(hasReceiver ? View.VISIBLE : View.GONE);
        final TextView close = findViewById(com.android.internal.R.id.aerr_close);
        close.setOnClickListener(this);
        final TextView appInfo = findViewById(com.android.internal.R.id.aerr_app_info);
        appInfo.setOnClickListener(this);

        boolean showMute = !Build.IS_USER && Settings.Global.getInt(context.getContentResolver(),
                Settings.Global.DEVELOPMENT_SETTINGS_ENABLED, 0) != 0
                && Settings.Global.getInt(context.getContentResolver(),
                Settings.Global.SHOW_MUTE_IN_CRASH_DIALOG, 0) != 0;
        final TextView mute = findViewById(com.android.internal.R.id.aerr_mute);
        mute.setOnClickListener(this);
        mute.setVisibility(showMute ? View.VISIBLE : View.GONE);

        findViewById(com.android.internal.R.id.customPanel).setVisibility(View.VISIBLE);
    }

    @Override
    public void onStart() {
        super.onStart();
        getContext().registerReceiver(mReceiver,
                new IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
    }

    @Override
    protected void onStop() {
        super.onStop();
        getContext().unregisterReceiver(mReceiver);
    }

    private final Handler mHandler = new Handler() {
        public void handleMessage(Message msg) {
            setResult(msg.what);
            dismiss();
        }
    };

    @Override
    public void dismiss() {
        if (!mResult.mHasResult) {
            // We are dismissing and the result has not been set...go ahead and set.
            setResult(FORCE_QUIT);
        }
        super.dismiss();
    }

    private void setResult(int result) {
        synchronized (mService) {
            if (mProc != null && mProc.crashDialog == AppErrorDialog.this) {
                mProc.crashDialog = null;
            }
        }
        mResult.set(result);

        // Make sure we don't have time timeout still hanging around.
        mHandler.removeMessages(TIMEOUT);
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case com.android.internal.R.id.aerr_restart:
                mHandler.obtainMessage(RESTART).sendToTarget();
                break;
            case com.android.internal.R.id.aerr_report:
                mHandler.obtainMessage(FORCE_QUIT_AND_REPORT).sendToTarget();
                break;
            case com.android.internal.R.id.aerr_close:
                mHandler.obtainMessage(FORCE_QUIT).sendToTarget();
                break;
            case com.android.internal.R.id.aerr_app_info:
                mHandler.obtainMessage(APP_INFO).sendToTarget();
                break;
            case com.android.internal.R.id.aerr_mute:
                mHandler.obtainMessage(MUTE).sendToTarget();
                break;
            default:
                break;
        }
    }

private void setResult(int result) {
        synchronized (mService) {
            if (mProc != null && mProc.crashDialog == AppErrorDialog.this) {
                mProc.crashDialog = null;
            }
        }
        mResult.set(result);

        // Make sure we don't have time timeout still hanging around.
        mHandler.removeMessages(TIMEOUT);
    }

final class AppErrorResult {
    public void set(int res) {
        synchronized (this) {
            mHasResult = true;
            mResult = res;
            notifyAll();
        }
    }

    public int get() {
        synchronized (this) {
            while (!mHasResult) {
                try {
                    wait();
                } catch (InterruptedException e) {
                }
            }
        }
        return mResult;
    }

    boolean mHasResult = false;
    int mResult;
}

在AppErrorDialog中,从源码中就是在app崩溃的时候,弹窗弹出停止运行和查看应用详情这两个选项,所以可以从查看应用详情的相关源码注释掉相关源码


3.2 AppErrors.java的相关代码

void crashApplication(ProcessRecord r, ApplicationErrorReport.CrashInfo crashInfo) {
        final int callingPid = Binder.getCallingPid();
        final int callingUid = Binder.getCallingUid();

        final long origId = Binder.clearCallingIdentity();
        try {
            crashApplicationInner(r, crashInfo, callingPid, callingUid);
        } finally {
            Binder.restoreCallingIdentity(origId);
        }
    }

    void crashApplicationInner(ProcessRecord r, ApplicationErrorReport.CrashInfo crashInfo,
            int callingPid, int callingUid) {
        long timeMillis = System.currentTimeMillis();
        String shortMsg = crashInfo.exceptionClassName;
        String longMsg = crashInfo.exceptionMessage;
        String stackTrace = crashInfo.stackTrace;
        if (shortMsg != null && longMsg != null) {
            longMsg = shortMsg + ": " + longMsg;
        } else if (shortMsg != null) {
            longMsg = shortMsg;
        }

        if (r != null) {
            boolean isApexModule = false;
            try {
                for (String androidPackage : r.getPackageList()) {
                    ModuleInfo moduleInfo = mContext.getPackageManager().getModuleInfo(
                            androidPackage, /*flags=*/ 0);
                    if (moduleInfo != null) {
                        isApexModule = true;
                        break;
                    }
                }
            } catch (IllegalStateException | PackageManager.NameNotFoundException e) {
                // Call to PackageManager#getModuleInfo() can result in NameNotFoundException or
                // IllegalStateException. In case they are thrown, there isn't much we can do
                // other than proceed with app crash handling.
            }

            if (r.isPersistent() || isApexModule) {
                // If a persistent app or apex module is stuck in a crash loop, the device isn't
                // very usable, so we want to consider sending out a rescue party.
                RescueParty.noteAppCrash(mContext, r.uid);
            }

            mPackageWatchdog.onPackageFailure(r.getPackageListWithVersionCode());
        }

        final int relaunchReason = r != null
                ? r.getWindowProcessController().computeRelaunchReason() : RELAUNCH_REASON_NONE;

        AppErrorResult result = new AppErrorResult();
        int taskId;
        synchronized (mService) {
            /**
             * If crash is handled by instance of {@link android.app.IActivityController},
             * finish now and don't show the app error dialog.
             */
            if (handleAppCrashInActivityController(r, crashInfo, shortMsg, longMsg, stackTrace,
                    timeMillis, callingPid, callingUid)) {
                return;
            }

            // Suppress crash dialog if the process is being relaunched due to a crash during a free
            // resize.
            if (relaunchReason == RELAUNCH_REASON_FREE_RESIZE) {
                return;
            }

            /**
             * If this process was running instrumentation, finish now - it will be handled in
             * {@link ActivityManagerService#handleAppDiedLocked}.
             */
            if (r != null && r.getActiveInstrumentation() != null) {
                return;
            }

            // Log crash in battery stats.
            if (r != null) {
                mService.mBatteryStatsService.noteProcessCrash(r.processName, r.uid);
            }

            AppErrorDialog.Data data = new AppErrorDialog.Data();
            data.result = result;
            data.proc = r;

            // If we can't identify the process or it's already exceeded its crash quota,
            // quit right away without showing a crash dialog.
            if (r == null || !makeAppCrashingLocked(r, shortMsg, longMsg, stackTrace, data)) {
                return;
            }

            final Message msg = Message.obtain();
            msg.what = ActivityManagerService.SHOW_ERROR_UI_MSG;

            taskId = data.taskId;
            msg.obj = data;
            mService.mUiHandler.sendMessage(msg);
        }

        int res = result.get();

        Intent appErrorIntent = null;
        MetricsLogger.action(mContext, MetricsProto.MetricsEvent.ACTION_APP_CRASH, res);
        if (res == AppErrorDialog.TIMEOUT || res == AppErrorDialog.CANCEL) {
            res = AppErrorDialog.FORCE_QUIT;
        }
        synchronized (mService) {
            if (res == AppErrorDialog.MUTE) {
                stopReportingCrashesLocked(r);
            }
            if (res == AppErrorDialog.RESTART) {
                mService.mProcessList.removeProcessLocked(r, false, true, "crash");
                if (taskId != INVALID_TASK_ID) {
                    try {
                        mService.startActivityFromRecents(taskId,
                                ActivityOptions.makeBasic().toBundle());
                    } catch (IllegalArgumentException e) {
                        // Hmm...that didn't work. Task should either be in recents or associated
                        // with a stack.
                        Slog.e(TAG, "Could not restart taskId=" + taskId, e);
                    }
                }
            }
            if (res == AppErrorDialog.FORCE_QUIT) {
                long orig = Binder.clearCallingIdentity();
                try {
                    // Kill it with fire!
                    mService.mAtmInternal.onHandleAppCrash(r.getWindowProcessController());
                    if (!r.isPersistent()) {
                        mService.mProcessList.removeProcessLocked(r, false, false, "crash");
                        mService.mAtmInternal.resumeTopActivities(false /* scheduleIdle */);
                    }
                } finally {
                    Binder.restoreCallingIdentity(orig);
                }
            }
            if (res == AppErrorDialog.APP_INFO) {
                appErrorIntent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
                appErrorIntent.setData(Uri.parse("package:" + r.info.packageName));
                appErrorIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            }
            if (res == AppErrorDialog.FORCE_QUIT_AND_REPORT) {
                appErrorIntent = createAppErrorIntentLocked(r, timeMillis, crashInfo);
            }
            if (r != null && !r.isolated && res != AppErrorDialog.RESTART) {
                // XXX Can't keep track of crash time for isolated processes,
                // since they don't have a persistent identity.
                mProcessCrashTimes.put(r.info.processName, r.uid,
                        SystemClock.uptimeMillis());
            }
        }

        if (appErrorIntent != null) {
            try {
                mContext.startActivityAsUser(appErrorIntent, new UserHandle(r.userId));
            } catch (ActivityNotFoundException e) {
                Slog.w(TAG, "bug report receiver dissappeared", e);
            }
        }
    }

从这段代码可以看出调用的是Settings.ACTION_APPLICATION_DETAILS_SETTINGS的intent应用信息页调转
            if (res == AppErrorDialog.APP_INFO) {
                appErrorIntent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
                appErrorIntent.setData(Uri.parse("package:" + r.info.packageName));
                appErrorIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            }
最后通过mContext.startActivityAsUser(appErrorIntent, new UserHandle(r.userId));来调整到应用信息页

3.3 ContextImpl.java关于启动Activity的相关代码调用

  class ContextImpl extends Context {
    private final static String TAG = "ContextImpl";
    private final static boolean DEBUG = false;

    private static final String XATTR_INODE_CACHE = "user.inode_cache";
    private static final String XATTR_INODE_CODE_CACHE = "user.inode_code_cache";

    /**
     * Map from package name, to preference name, to cached preferences.
     */
    @GuardedBy("ContextImpl.class")
    @UnsupportedAppUsage
    private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;

    /**
     * Map from preference name to generated path.
     */
    @GuardedBy("ContextImpl.class")
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
    private ArrayMap<String, File> mSharedPrefsPaths;

    @UnsupportedAppUsage
    final @NonNull ActivityThread mMainThread;
    @UnsupportedAppUsage
    final @NonNull LoadedApk mPackageInfo;
    @UnsupportedAppUsage
    private @Nullable ClassLoader mClassLoader;

    private final @Nullable IBinder mActivityToken;

    private final @NonNull UserHandle mUser;

@UnsupportedAppUsage
    static ContextImpl getImpl(Context context) {
        Context nextContext;
        while ((context instanceof ContextWrapper) &&
                (nextContext=((ContextWrapper)context).getBaseContext()) != null) {
            context = nextContext;
        }
        return (ContextImpl)context;
    }

    @Override
    public AssetManager getAssets() {
        return getResources().getAssets();
    }

    @Override
    public Resources getResources() {
        return mResources;
    }

    @Override
    public PackageManager getPackageManager() {
        if (mPackageManager != null) {
            return mPackageManager;
        }

        IPackageManager pm = ActivityThread.getPackageManager();
        if (pm != null) {
            // Doesn't matter if we make more than one instance.
            return (mPackageManager = new ApplicationPackageManager(this, pm));
        }

        return null;
    }

    @Override
    public ContentResolver getContentResolver() {
        return mContentResolver;
    }

    @Override
    public Looper getMainLooper() {
        return mMainThread.getLooper();
    }

    @Override
    public Executor getMainExecutor() {
        return mMainThread.getExecutor();
    }

    @Override
    public Context getApplicationContext() {
        return (mPackageInfo != null) ?
                mPackageInfo.getApplication() : mMainThread.getApplication();
    }

    @Override
    public void setTheme(int resId) {
        synchronized (mSync) {
            if (mThemeResource != resId) {
                mThemeResource = resId;
                initializeTheme();
            }
        }
    }

    @Override
    public int getThemeResId() {
        synchronized (mSync) {
            return mThemeResource;
        }
    }

    @Override
    public Resources.Theme getTheme() {
        synchronized (mSync) {
            if (mTheme != null) {
                return mTheme;
            }

            mThemeResource = Resources.selectDefaultTheme(mThemeResource,
                    getOuterContext().getApplicationInfo().targetSdkVersion);
            initializeTheme();

            return mTheme;
        }
    }

    private void initializeTheme() {
        if (mTheme == null) {
            mTheme = mResources.newTheme();
        }
        mTheme.applyStyle(mThemeResource, true);
    }

    @Override
    public ClassLoader getClassLoader() {
        return mClassLoader != null ? mClassLoader : (mPackageInfo != null ? mPackageInfo.getClassLoader() : ClassLoader.getSystemClassLoader());
    }

    @Override
    public String getPackageName() {
        if (mPackageInfo != null) {
            return mPackageInfo.getPackageName();
        }
        // No mPackageInfo means this is a Context for the system itself,
        // and this here is its name.
        return "android";
    }

    /** @hide */
    @Override
    public String getBasePackageName() {
        return mBasePackageName != null ? mBasePackageName : getPackageName();
    }

@Override
    public void startActivity(Intent intent) {
        // add core start
        if(intent==null)return;
		String action = intent.getAction();
		if(DEBUG)Log.e("ContextImpl","startActivity--action:"+action);
		if(action!=null&&action.equals("android.settings.APPLICATION_DETAILS_SETTINGS"))return;
      //add core end
        warnIfCallingFromSystemProcess();
        startActivity(intent, null);
    }

    /** @hide */
    @Override
    public void startActivityAsUser(Intent intent, UserHandle user) {
       // add core start
 if(intent==null)return;
		String action = intent.getAction();
		if(DEBUG)Log.e("ContextImpl","startActivityAsUser--action:"+action);
		if(action!=null&&action.equals("android.settings.APPLICATION_DETAILS_SETTINGS"))return;
      //add core end
        startActivityAsUser(intent, null, user);
    }

}

在ContextImpl.java的startActivity(Intent intent)和startActivityAsUser(Intent intent, UserHandle user)中判断是否是关于应用信息的启动信息 然后禁止启动就行了

3.4 Activity.java中关于禁止启动应用信息的相关代码

/**
     * Same as {@link #startActivity(Intent, Bundle)} with no options
     * specified.
     *
     * @param intent The intent to start.
     *
     * @throws android.content.ActivityNotFoundException
     *
     * @see #startActivity(Intent, Bundle)
     * @see #startActivityForResult
     */
    @Override
    public void startActivity(Intent intent) {
        // add core start
        if(intent==null)return;
		String action = intent.getAction();
		if(DEBUG_LIFECYCLE)Log.e("Activity","startActivity--action:"+action);
		if(action!=null&&action.equals("android.settings.APPLICATION_DETAILS_SETTINGS"))return;
        //add core end
        this.startActivity(intent, null);
    }

    /**
     * @hide Implement to provide correct calling token.
     */
    @Override
    public void startActivityAsUser(Intent intent, UserHandle user) {
        // add core start
        if(intent==null)return;
        String action = intent.getAction();
		if(DEBUG_LIFECYCLE)Log.e("Activity","action:"+action);
		if(action!=null&&action.equals("android.settings.APPLICATION_DETAILS_SETTINGS"))return;
        //add core end
        startActivityAsUser(intent, null, user);
    }

在Activity中的startActivity(Intent intent)和startActivityAsUser(Intent intent, UserHandle user)中判断是否是关于应用信息的启动信息 然后禁止启动就行了,这样就达到了禁用查看应用详情的功能

### 回答1: android 10.0系统应用默认授权是指在android 10.0系统中,部分应用在安装后默认会被授予一些权限,而不需要用户在应用启动后再去手动开启这些权限,这样可以提高用户的使用体验。 android 10.0系统应用默认授权的目的是为了加强应用的安全性,提高用户的隐私保护。应用需要访问某些敏感数据或功能时,用户在使用前需要手动开启权限,这样可以减少恶意应用通过获取用户授权的方式获取用户的私人信息。 在android 10.0系统中,应用默认被授权的权限包括日历、相机、联系人、位置、麦克风、电话、短信、存储空间等。这些权限是应用正常运行所必需的,用户可以在应用管理器中查看和管理应用授权的权限。 尽管android 10.0系统应用默认授权提高了用户的使用体验,但也存在一些风险。如果用户使用的是恶意应用,这些应用也可以默认获取一些敏感权限,从而获取用户的私人信息。因此,用户需要保持对应用的警惕,并仔细审核应用的权限请求。 ### 回答2: 在Android 10.0系统中,应用默认授权是一种新的权限模型。它改变了以往Android系统中权限管理的方式,使用户对应用程序的权限管理更加方便和安全。 传统的权限管理模型通常是一次性处理权限请求,无法区分应用程序对某一项权限的使用情况。这种方式缺少细节和灵活性,可能导致应用程序通过某些权限去访问用户的隐私信息。而Android 10.0系统应用默认授权则可以保护用户的隐私信息和数据安全。 应用默认授权模型允许应用程序在不请求用户手动授权的情况下,自动获得某些权限,并在必要的时候再向用户请求授权。这种方式可以减少用户被安装并包含恶意代码的应用程序所利用的风险。 具体地说,当应用程序请求任何运行时权限时,Android 10.0系统将无法立即授予权限。相反,它将显示一个对话框,询问用户是否要授予权限。只有在用户同意授予权限时,应用程序才能获得此权限。 此外,应用默认授权模型还能够自动限制应用程序对某些权限的访问,比如位置信息和网络数据,除非用户主动授予权限或应用程序已经获得了相应的批准。 总之,Android 10.0系统应用默认授权能够保护用户隐私,增加应用程序的灵活性,并减少用户遭受来自恶意应用程序的风险。 ### 回答3: Android 10.0系统应用默认授权,是指所有应用程序在安装时系统会默认授予其部分权限,而不是像以前版本的系统一样需要用户在应用使用时手动授权。 Android 10.0系统应用默认授权的目的,是为了提高应用程序的用户便利性和操作流畅性,同时还可以降低用户在使用应用程序时需要授权的次数。但是这也有可能导致一些应用程序获取用户隐私的风险。 在Android 10.0系统中,应用程序默认被授权的权限包括:网络访问、用户日历、联系人和传感器等。对于其他一些权限,如摄像头、麦克风、存储空间和定位等信息,用户在应用程序使用时需要手动授权才能访问。 因此,用户在使用Android 10.0系统时需要非常谨慎地选择哪些应用程序可以获得自己的隐私权限。建议用户在安装应用程序前,仔细检查其权限请求,尽可能减少不必要的隐私授权操作。 同时,为了更好地保护用户的隐私,Android 10.0系统也提供了更加严格的权限管理功能。用户可以通过设置菜单中的 “应用程序和通知”-“应用程序权限” 来查看和修改应用程序的权限,在此基础上更好地控制应用程序的使用和权限访问
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

安卓兼职framework应用工程师

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值