Android N BlockedNumberContract原生黑名单(一)

Google 从Android N 开始从原生上支持号码屏蔽也就是电话黑名单功能,相关原文翻译如下:
 
 号码屏蔽 
Android N 现在支持在平台中进行号码屏蔽,提供框架 API,让服务提供商可以维护屏蔽的号码列表。 默认短信应用、默认手机应用和提供商应用可以对屏蔽的号码列表进行读取和写入操作。 其他应用则无法访问此列表。
通过使号码屏蔽成为平台的标准功能,Android 为应用提供一致的方式来支持广泛的设备上的号码屏蔽。 应用可以利用的其他优势包括:
屏蔽已屏蔽的来电号码发出的短信
通过 Backup &; Restore(备份和还原)功能可以跨重置和设备保留屏蔽的号码
多个应用可以使用相同的屏蔽号码列表 此外,通过 Android 的运营商应用集成表示运营商可以读取设备上屏蔽的号码列表,并为用户执行服务端屏蔽,以阻止不需要的来电和短信通过任何介质(如 VOIP 端点或转接电话)到达用户。
如需了解详细信息,请参阅可下载的 API 参考中的 android.provider.BlockedNumberContract。

来电拦截 
Android N 允许默认的手机应用过滤来电。手机应用执行此操作的方式是实现新的 CallScreeningService,该方法允许手机应用基于来电的 Call.Details 执行大量操作,例如:

拒绝来电
不允许来电到达通话记录
不向用户显示来电通知 如需了解详细信息,请参阅可下载的 API 参考中的 android.telecom.CallScreeningService。 
 
新增的两个API:
 android.provider.BlockedNumberContract
 android.telecom.CallScreeningService
 
  /frameworks/base/core/java/android/provider/BlockedNumberContract.java中有两个重要方法
 
 /**
     * Returns whether a given number is in the blocked list.
     *
     * <p> This matches the {@code phoneNumber} against the
     * {@link BlockedNumbers#COLUMN_ORIGINAL_NUMBER} column, and the E164 representation of the
     * {@code phoneNumber} with the {@link BlockedNumbers#COLUMN_E164_NUMBER} column.
     *
     * <p> Note that if the {@link #canCurrentUserBlockNumbers} is {@code false} for the user
     * context {@code context}, this method will throw a {@link SecurityException}.
     *
     * @return {@code true} if the {@code phoneNumber} is blocked.
     */
    @WorkerThread
    public static boolean isBlocked(Context context, String phoneNumber) {
        final Bundle res = context.getContentResolver().call(
                AUTHORITY_URI, METHOD_IS_BLOCKED, phoneNumber, null);
        return res != null && res.getBoolean(RES_NUMBER_IS_BLOCKED, false);
    }

/**
     * Unblocks the {@code phoneNumber} if it is blocked.
     *
     * <p> This deletes all rows where the {@code phoneNumber} matches the
     * {@link BlockedNumbers#COLUMN_ORIGINAL_NUMBER} column or the E164 representation of the
     * {@code phoneNumber} matches the {@link BlockedNumbers#COLUMN_E164_NUMBER} column.
     *
     * <p>To delete rows based on exact match with specific columns such as
     * {@link BlockedNumbers#COLUMN_ID} use
     * {@link android.content.ContentProvider#delete(Uri, String, String[])} with
     * {@link BlockedNumbers#CONTENT_URI} URI.
     *
     * <p> Note that if the {@link #canCurrentUserBlockNumbers} is {@code false} for the user
     * context {@code context}, this method will throw a {@link SecurityException}.
     *
     * @return the number of rows deleted in the blocked number provider as a result of unblock.
     */
    @WorkerThread
    public static int unblock(Context context, String phoneNumber) {
        final Bundle res = context.getContentResolver().call(
                AUTHORITY_URI, METHOD_UNBLOCK, phoneNumber, null);
        return res.getInt(RES_NUM_ROWS_DELETED, 0);
    }

isBlocked是判断号码是否在黑名单列表中,存在返回true,unblock是解除拦截,从黑名单列表删除号码

通过在framework中搜索 BlockedNumberContract类名查找被调用的地方,发现有2处:

/frameworks/opt/telephony/src/java/com/android/internal/telephony/AsyncEmergencyContactNotifier.java
/frameworks/opt/telephony/src/java/com/android/internal/telephony/BlockChecker.java

从类名可知前一个是和emergency call相关的,我们暂且不管,看第二个

public class BlockChecker {
    private static final String TAG = "BlockChecker";
    private static final boolean VDBG = false; // STOPSHIP if true.

/**
     * Returns {@code true} if {@code phoneNumber} is blocked.
     * <p>
     * This method catches all underlying exceptions to ensure that this method never throws any
     * exception.
     */
    public static boolean isBlocked(Context context, String phoneNumber) {
        boolean isBlocked = false;
        long startTimeNano = System.nanoTime();

try {
            if (BlockedNumberContract.SystemContract.shouldSystemBlockNumber(
                    context, phoneNumber)) {
                Rlog.d(TAG, phoneNumber + " is blocked.");
                isBlocked = true;
            }
        } catch (Exception e) {
            Rlog.e(TAG, "Exception checking for blocked number: " + e);
        }

int durationMillis = (int) ((System.nanoTime() - startTimeNano) / 1000000);
        if (durationMillis > 500 || VDBG) {
            Rlog.d(TAG, "Blocked number lookup took: " + durationMillis + " ms.");
        }
        return isBlocked;
    }
}
 
此类调用了 BlockedNumberContract.SystemContract.shouldSystemBlockNumber(context, phoneNumber)方法,该方法也是
判断号码是否处在黑名单列表中。BlockChecker类很重要,他是framework层判断是否拦截来电和短信的主要类,稍后会着重分析。

我们现在看下在application 层到底是那些应用会用到BlockedNumberContract 这个API,和通话相关的我们首先看Dialer应用,
发现在dialer的settings中有call blocking设置,这里的布局非常简单,一个ADD A NUMBER按钮,点击弹出输入框输入要拦截的号码,
我们使用布局分析工具hierarchyviewer.bat迅速定位到相关代码在package/services/Telecom中,添加号码的Activity为
/packages/services/Telecomm/src/com/android/server/telecom/settings/BlockedNumbersActivity.java

添加号码的对话框代码为 
   private void showAddBlockedNumberDialog() {
        LayoutInflater inflater = this.getLayoutInflater();
        View dialogView = inflater.inflate(R.xml.add_blocked_number_dialog, null);
        final EditText editText = (EditText) dialogView.findViewById(R.id.add_blocked_number);
        editText.addTextChangedListener(new PhoneNumberFormattingTextWatcher());
        editText.addTextChangedListener(this);
        AlertDialog dialog = new AlertDialog.Builder(this)
                .setView(dialogView)
                .setPositiveButton(R.string.block_button, new DialogInterface.OnClickListener() {
                    public void onClick(DialogInterface dialog, int id) {
                        addBlockedNumber(PhoneNumberUtils.stripSeparators(
                                editText.getText().toString()));
                    }
                })
                .setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
                    public void onClick(DialogInterface dialog, int id) {
                        dialog.dismiss();
                    }
                })
                .create();
        dialog.setOnShowListener(new AlertDialog.OnShowListener() {
                    @Override
                    public void onShow(DialogInterface dialog) {
                        mBlockButton = ((AlertDialog) dialog)
                                .getButton(AlertDialog.BUTTON_POSITIVE);
                        mBlockButton.setEnabled(false);
                        // show keyboard
                        InputMethodManager inputMethodManager =
                                (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
                        inputMethodManager.showSoftInput(editText,
                                InputMethodManager.SHOW_IMPLICIT);

}
                });
        dialog.show();
    }

其中调用的addBlockedNumber(PhoneNumberUtils.stripSeparators(editText.getText().toString()));方法:
 /**
     * Add blocked number if it does not exist.
     */
    private void addBlockedNumber(String number) {
        if (PhoneNumberUtils.isEmergencyNumber(number)) {
            Toast.makeText(
                    this,
                    getString(R.string.blocked_numbers_block_emergency_number_message),
                    Toast.LENGTH_SHORT).show();
        } else {
            // We disable the add button, to prevent the user from adding other numbers until the
            // current number is added.
            mAddButton.setEnabled(false);
            mBlockNumberTaskFragment.blockIfNotAlreadyBlocked(number, this);
        }
    }

可见添加号码前判断此号码是否是紧急号码,不是的话会调用mBlockNumberTaskFragment的blockIfNotAlreadyBlocked方法,
进入/packages/services/Telecomm/src/com/android/server/telecom/settings/BlockNumberTaskFragment.java
blockIfNotAlreadyBlocked方法:
/**
     * Runs an async task to write the number to the blocked numbers provider if it does not already
     * exist.
     *
     * Triggers {@link Listener#onBlocked(String, boolean)} when task finishes to show proper UI.
     */
    public void blockIfNotAlreadyBlocked(String number, Listener listener) {
        mListener = listener;
        mTask = new BlockNumberTask();
        mTask.execute(number);
    }

通过一个异步任务进行添加

/**
     * Task to block a number.
     */
    private class BlockNumberTask extends AsyncTask<String, Void, Boolean> {
        private String mNumber;

/**
         * @return true if number was blocked; false if number is already blocked.
         */
        @Override
        protected Boolean doInBackground(String... params) {
            mNumber = params[0];
            if (BlockedNumberContract.isBlocked(getContext(), mNumber)) {
                return false;
            } else {
                ContentResolver contentResolver = getContext().getContentResolver();
                ContentValues newValues = new ContentValues();
                newValues.put(BlockedNumberContract.BlockedNumbers.COLUMN_ORIGINAL_NUMBER,
                        mNumber);
                contentResolver.insert(BlockedNumberContract.BlockedNumbers.CONTENT_URI,
                        newValues);
                return true;
            }
        }

@Override
        protected void onPostExecute(Boolean result) {
            mTask = null;
            if (mListener != null) {
                mListener.onBlocked(mNumber, !result /* alreadyBlocked */);
            }
            mListener = null;
        }
    }

在doInBackground方法中我们发现在添加添加号码前调用了我们前文所述的BlockedNumberContract的isBlocked方法来判断
此号码是否已经是黑名单中的号码,黑名单表中不存在此号码则使用ContentResolver将号码插入数据库中。

黑名单数据库的相关代码在/packages/providers/BlockedNumberProvider/中,这个provider代码相对简单,只有4个类
BlockedNumberBackupAgent.java
BlockedNumberDatabaseHelper.java
BlockedNumberProvider.java
Utils.java

BlockedNumberDatabaseHelper.java是sql helper类,创建黑名单数据库

private static final String DATABASE_NAME = "blockednumbers.db";
public interface Tables {
        String BLOCKED_NUMBERS = "blocked";
    }
  private void createTables(SQLiteDatabase db) {
            db.execSQL("CREATE TABLE " + Tables.BLOCKED_NUMBERS + " (" +
                    BlockedNumbers.COLUMN_ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
                    BlockedNumbers.COLUMN_ORIGINAL_NUMBER + " TEXT NOT NULL UNIQUE," +
                    BlockedNumbers.COLUMN_E164_NUMBER + " TEXT" +
                    ")");
            db.execSQL("CREATE INDEX blocked_number_idx_original ON " + Tables.BLOCKED_NUMBERS +
                    " (" + BlockedNumbers.COLUMN_ORIGINAL_NUMBER + ");");
            db.execSQL("CREATE INDEX blocked_number_idx_e164 ON " + Tables.BLOCKED_NUMBERS + " (" +
                    BlockedNumbers.COLUMN_E164_NUMBER +
                    ");");
        }

数据库名blockednumbers.db,表名blocked,表字段有BlockedNumbers.COLUMN_ID,BlockedNumbers.COLUMN_ORIGINAL_NUMBER和
BlockedNumbers.COLUMN_E164_NUMBER,这3个字段都定义在BlockedNumberContract类中,其中COLUMN_E164_NUMBER是国际化后的号码,
即如中国号码在前加上 +86 后的号码形式。

黑名单数据库增删改查操作类BlockedNumberProvider.java,这个类中方法的方法名很多和BlockedNumberContract中的方法对应着,
因为provider的方法是在BlockedNumberContract中被调用的,provider中的重要方法是
 @Override
    public Bundle call(@NonNull String method, @Nullable String arg, @Nullable Bundle extras) {
        final Bundle res = new Bundle();
        switch (method) {
            case BlockedNumberContract.METHOD_IS_BLOCKED:
                enforceReadPermissionAndPrimaryUser();

res.putBoolean(BlockedNumberContract.RES_NUMBER_IS_BLOCKED, isBlocked(arg));
                break;
            case BlockedNumberContract.METHOD_CAN_CURRENT_USER_BLOCK_NUMBERS:
                // No permission checks: any app should be able to access this API.
                res.putBoolean(
                        BlockedNumberContract.RES_CAN_BLOCK_NUMBERS, canCurrentUserBlockUsers());
                break;
            case BlockedNumberContract.METHOD_UNBLOCK:
                enforceWritePermissionAndPrimaryUser();

res.putInt(BlockedNumberContract.RES_NUM_ROWS_DELETED, unblock(arg));
                break;
            case SystemContract.METHOD_NOTIFY_EMERGENCY_CONTACT:
                enforceSystemWritePermissionAndPrimaryUser();

notifyEmergencyContact();
                break;
            case SystemContract.METHOD_END_BLOCK_SUPPRESSION:
                enforceSystemWritePermissionAndPrimaryUser();

endBlockSuppression();
                break;
            case SystemContract.METHOD_GET_BLOCK_SUPPRESSION_STATUS:
                enforceSystemReadPermissionAndPrimaryUser();

SystemContract.BlockSuppressionStatus status = getBlockSuppressionStatus();
                res.putBoolean(SystemContract.RES_IS_BLOCKING_SUPPRESSED, status.isSuppressed);
                res.putLong(SystemContract.RES_BLOCKING_SUPPRESSED_UNTIL_TIMESTAMP,
                        status.untilTimestampMillis);
                break;
            case SystemContract.METHOD_SHOULD_SYSTEM_BLOCK_NUMBER:
                enforceSystemReadPermissionAndPrimaryUser();
                res.putBoolean(
                        BlockedNumberContract.RES_NUMBER_IS_BLOCKED, shouldSystemBlockNumber(arg));
                break;
            default:
                enforceReadPermissionAndPrimaryUser();

throw new IllegalArgumentException("Unsupported method " + method);
        }
        return res;
    }

在BlockedNumberContract.java的isBlocked方法中看出是这样子调用的
Bundle res = context.getContentResolver().call(
                AUTHORITY_URI, METHOD_IS_BLOCKED, phoneNumber, null);
代码进入BlockedNumberProvider.java的call方法,对应着
case BlockedNumberContract.METHOD_IS_BLOCKED:
                enforceReadPermissionAndPrimaryUser();

res.putBoolean(BlockedNumberContract.RES_NUMBER_IS_BLOCKED, isBlocked(arg));
                break;

我们看这类里的isBlocked方法

private boolean isBlocked(String phoneNumber) {
        if (TextUtils.isEmpty(phoneNumber)) {
            return false;
        }

final String inE164 = Utils.getE164Number(getContext(), phoneNumber, null); // may be empty.

if (DEBUG) {
            Log.d(TAG, String.format("isBlocked: in=%s, e164=%s", phoneNumber, inE164));
        }

final Cursor c = mDbHelper.getReadableDatabase().rawQuery(
                "SELECT " +
                BlockedNumberContract.BlockedNumbers.COLUMN_ORIGINAL_NUMBER + "," +
                BlockedNumberContract.BlockedNumbers.COLUMN_E164_NUMBER +
                " FROM " + BlockedNumberDatabaseHelper.Tables.BLOCKED_NUMBERS +
                " WHERE " + BlockedNumberContract.BlockedNumbers.COLUMN_ORIGINAL_NUMBER + "=?1" +
                " OR (?2 != '' AND " +
                        BlockedNumberContract.BlockedNumbers.COLUMN_E164_NUMBER + "=?2)",
                new String[] {phoneNumber, inE164}
                );
        try {
            while (c.moveToNext()) {
                if (DEBUG) {
                    final String original = c.getString(0);
                    final String e164 = c.getString(1);

Log.d(TAG, String.format("match found: original=%s, e164=%s", original, e164));
                }
                return true;
            }
        } finally {
            c.close();
        }
        // No match found.
        return false;
    }

查询了数据库BlockedNumberDatabaseHelper.Tables.BLOCKED_NUMBERS,条件为原始号码或国际化后的号码。

我们前文提到BlockChecker.java的isBlocked方法是framework层判断拦截与否的重要方法,那么我们查下framework
层那些类调用到它,经过搜索发现有3处地方调用,分别如下
/packages/services/Telecomm/src/com/android/server/telecom/callfiltering/BlockCheckerAdapter.java
/frameworks/opt/telephony/src/java/com/android/internal/telephony/WapPushOverSms.java
/frameworks/opt/telephony/src/java/com/android/internal/telephony/InboundSmsHandler.java

从类名可以看出前一个BlockCheckerAdapter.java是和call相关的,后两个是和sms相关的

BlockCheckerAdapter.java

public class BlockCheckerAdapter {
    public BlockCheckerAdapter() { }

public boolean isBlocked(Context context, String number) {
        return BlockChecker.isBlocked(context, number);
    }
}
这个类很简单,直接调用了BlockChecker的isBlocked方法,继续看BlockCheckerAdapter的调用地方
/packages/services/Telecomm/src/com/android/server/telecom/callfiltering/AsyncBlockCheckFilter.java
/packages/services/Telecomm/src/com/android/server/telecom/CallsManager.java

CallsManager.java
    @Override
    public void onSuccessfulIncomingCall(Call incomingCall) {
        Log.d(this, "onSuccessfulIncomingCall");
        if (incomingCall.hasProperty(Connection.PROPERTY_EMERGENCY_CALLBACK_MODE)) {
            Log.i(this, "Skipping call filtering due to ECBM");
            onCallFilteringComplete(incomingCall, new CallFilteringResult(true, false, true, true));
            return;
        }

List<IncomingCallFilter.CallFilter> filters = new ArrayList<>();
        filters.add(new DirectToVoicemailCallFilter(mCallerInfoLookupHelper));
        filters.add(new AsyncBlockCheckFilter(mContext, new BlockCheckerAdapter()));
        filters.add(new CallScreeningServiceFilter(mContext, this, mPhoneAccountRegistrar,
                mDefaultDialerManagerAdapter,
                new ParcelableCallUtils.Converter(), mLock));
        new IncomingCallFilter(mContext, this, incomingCall, mLock,
                mTimeoutsAdapter, filters).performFiltering();
    }

当一个call来到这个方法后会进行filter,3个filter条件通过电话流程才能继续走下去,
AsyncBlockCheckFilter.java

@Override
    protected Boolean doInBackground(String... params) {
        try {
            Log.continueSession(mBackgroundTaskSubsession, "ABCF.dIB");
            Log.event(mIncomingCall, Log.Events.BLOCK_CHECK_INITIATED);
            return mBlockCheckerAdapter.isBlocked(mContext, params[0]);
        } finally {
            Log.endSession();
        }
    }

@Override
    protected void onPostExecute(Boolean isBlocked) {
        Log.continueSession(mPostExecuteSubsession, "ABCF.oPE");
        try {
            CallFilteringResult result;
            if (isBlocked) {
                result = new CallFilteringResult(
                        false, // shouldAllowCall
                        true, //shouldReject
                        false, //shouldAddToCallLog
                        false // shouldShowNotification
                );
            } else {
                result = new CallFilteringResult(
                        true, // shouldAllowCall
                        false, // shouldReject
                        true, // shouldAddToCallLog
                        true // shouldShowNotification
                );
            }
            Log.event(mIncomingCall, Log.Events.BLOCK_CHECK_FINISHED, result);
            mCallback.onCallFilteringComplete(mIncomingCall, result);
        } finally {
            Log.endSession();
        }
    }

AsyncBlockCheckFilter.java中调用BlockCheckerAdapter的isBlocked方法,通过返回Boolean至构建
CallFilteringResult,CallFilteringResult的四个参考写的非常清楚

/packages/services/Telecomm/src/com/android/server/telecom/callfiltering/CallFilteringResult.java
 public boolean shouldAllowCall;//是否允许通话
    public boolean shouldReject;//是否拒绝通话
    public boolean shouldAddToCallLog;//是否填加到通话记录
    public boolean shouldShowNotification;//是否显示来电通知

public CallFilteringResult(boolean shouldAllowCall, boolean shouldReject, boolean
            shouldAddToCallLog, boolean shouldShowNotification) {
        this.shouldAllowCall = shouldAllowCall;
        this.shouldReject = shouldReject;
        this.shouldAddToCallLog = shouldAddToCallLog;
        this.shouldShowNotification = shouldShowNotification;
    }

构建完CallFilteringResult后调用通过callback将结果返回
mCallback.onCallFilteringComplete(mIncomingCall, result);
代码进入到CallsManager的onCallFilteringComplete方法
@Override
    public void onCallFilteringComplete(Call incomingCall, CallFilteringResult result) {
        // Only set the incoming call as ringing if it isn't already disconnected. It is possible
        // that the connection service disconnected the call before it was even added to Telecom, in
        // which case it makes no sense to set it back to a ringing state.
        if (incomingCall.getState() != CallState.DISCONNECTED &&
                incomingCall.getState() != CallState.DISCONNECTING) {
            setCallState(incomingCall, CallState.RINGING,
                    result.shouldAllowCall ? "successful incoming call" : "blocking call");
        } else {
            Log.i(this, "onCallFilteringCompleted: call already disconnected.");
            return;
        }

if (result.shouldAllowCall) {
            if (hasMaximumRingingCalls()) {
                Log.i(this, "onCallFilteringCompleted: Call rejected! Exceeds maximum number of " +
                        "ringing calls.");
                rejectCallAndLog(incomingCall);
            } else if (hasMaximumDialingCalls()) {
                Log.i(this, "onCallFilteringCompleted: Call rejected! Exceeds maximum number of " +
                        "dialing calls.");
                rejectCallAndLog(incomingCall);
            } else {
                addCall(incomingCall);
            }
        } else {
            if (result.shouldReject) {
                Log.i(this, "onCallFilteringCompleted: blocked call, rejecting.");
                incomingCall.reject(false, null);
            }
            if (result.shouldAddToCallLog) {
                Log.i(this, "onCallScreeningCompleted: blocked call, adding to call log.");
                if (result.shouldShowNotification) {
                    Log.w(this, "onCallScreeningCompleted: blocked call, showing notification.");
                }
                mCallLogManager.logCall(incomingCall, Calls.MISSED_TYPE,
                        result.shouldShowNotification);
            } else if (result.shouldShowNotification) {
                Log.i(this, "onCallScreeningCompleted: blocked call, showing notification.");
                mMissedCallNotifier.showMissedCallNotification(incomingCall);
            }
        }
    }

这里对回调回来的resulted进行判断,允许通话则调用addCall(incomingCall),拒绝通话调用incomingCall.reject(false, null);

到这里,电话的黑名单判断基本完了。我们来看下短信的
/frameworks/opt/telephony/src/java/com/android/internal/telephony/WapPushOverSms.java
 if (parsedPdu != null && parsedPdu.getMessageType() == MESSAGE_TYPE_NOTIFICATION_IND) {
                final NotificationInd nInd = (NotificationInd) parsedPdu;
                if (nInd.getFrom() != null
                        && BlockChecker.isBlocked(mContext, nInd.getFrom().getString())) {
                    result.statusCode = Intents.RESULT_SMS_HANDLED;
                    return result;
                }
            }

/frameworks/opt/telephony/src/java/com/android/internal/telephony/InboundSmsHandler.java
if (BlockChecker.isBlocked(mContext, tracker.getAddress())) {
            deleteFromRawTable(tracker.getDeleteWhere(), tracker.getDeleteWhereArgs(),
                    DELETE_PERMANENTLY);
            return false;
        }

短信方面不是很了解,这两处也只是调用了BlockChecker的isBlocked方法,具体处理流程以后有空再分析。

参考
 https://www.cnblogs.com/lance2016/archive/2016/11/06/6035351.html
 http://blog.csdn.net/michael_yt/article/details/54573087?utm_source=itdadao&utm_medium=referral

Android N BlockedNumberContract原生黑名单(一)相关推荐

  1. android7.0原生黑名单

    android7.0原生黑名单 1.电话短信黑名单号码判断isBlocked 文件位置 frameworks/opt/telephony/src/java/com/android/internal/t ...

  2. android游戏模式,注重游戏体验 Android 12提供原生游戏模式

    原标题:注重游戏体验 Android 12提供原生游戏模式 [PChome手机频道资讯报道]谷歌方面即将推出新版的Android 12系统,该系统代号Snow Cone,将采用全新的界面设计,并提供多 ...

  3. 三星原生android手机,非原生系统??三星Android Go手机曝光

    随着安卓版本的更新升级越来越快,安卓的碎片化丝毫没有减缓的迹象,即使是旗舰机型往往也只能获得约两个大版本更新,很多机型无法更新到最新版本的安卓系统.为了扩大中低端市场,让越来越多的人体验到原生安卓的便 ...

  4. android原生农场壁纸,Android 6.0高清壁纸下载-Android 6.0原生壁纸高清免费打包下载-东坡下载...

    android 6.0在现在是很多的安卓手机用户都是升级到这个版本的,那么你需要一些适合这个版本的高清主题壁纸吗?想要的话现在就赶快来下载吧! android 6.0新特性 新特性一:App Perm ...

  5. android 7原生动态壁纸,手机里都是定制OS?谷歌Android 7.0原生系统壁纸邀你来尝鲜!...

    原标题:手机里都是定制OS?谷歌Android 7.0原生系统壁纸邀你来尝鲜! 安卓系统的碎片化一直是被人们所诟病,苹果手机IOS系统升级的速度和占比一直都非常高,而安卓手机却相比都非常差,三星,华为 ...

  6. android9.0官方壁纸,安卓福利:全新Android 9.0原生壁纸 每一张都是谷歌的精挑细选!...

    原标题:安卓福利:全新Android 9.0原生壁纸 每一张都是谷歌的精挑细选! 至今,安卓碎片化的程度依然没有得到缓解,Android 8.0发布已经一年多,但却仅仅只有2%左右的用户成功尝到了奥利 ...

  7. 小米刷 android o,一代神机!五年前的小米2刷入Android 8.0原生系统

    五年前的小米手机2竟然能刷Android 8.0原生系统.做个比较,2012年推出的iPhone 5已经不能升级最新的iOS11了. 小米2是小米于2012年8月发布的一款产品,距今已经超过5年零三个 ...

  8. android keyevent列表,Android KeyEvent KeyCode 原生安卓代码对照表

    Android KeyEvent KeyCode 原生安卓代码对照表: public static final int FLAG_WOKE_HERE = 1; public static final  ...

  9. android原生屏蔽电话,更人性化 Android P获得原生屏蔽未知来电功能

    原标题:更人性化 Android P获得原生屏蔽未知来电功能 [PConline资讯]骚扰电话让人十分头痛,不过得益于各种拦截数据库的完善,基本上手机来电时都能自动识别来电是推销电话还是诈骗电话. 骚 ...

最新文章

  1. iOS 自定义UITabBar
  2. VMM系列之使用VMM服务器构建 Hyper-V 主机(3)
  3. 《分布式系统:概念与设计》一3.2 网络类型
  4. 无盘服务器 双镜像盘,镜像(无盘柜)-双活集群解决方案
  5. ⑨④-如果不发展就可能面临生存窘境
  6. 【图论】最短路上的统计(ssl 1500)
  7. Android仿ios二级菜单侧滑,仿IOS的列表项滑动菜单——ListItemMenu
  8. treemap比较器_Java TreeMap比较器()方法与示例
  9. js在ie追加html,如何使用JavaScript将属性添加/更新到HTML元素?
  10. 怎样查看cudnn版本_tensorflowGPU版本踩坑记录
  11. java对机房有什么用_使用云终端建设的云机房相比传统机房有什么区别
  12. idea断点调试继续执行快捷键(keymap设置了eclipse)
  13. aarch64 arm上交叉编译mysql-2.7.35
  14. 计算机考研各省份学校,想考研究生,哪个省份的高校更容易考上?
  15. 【笔记整理】数字信号处理复习——FT、DTFT、DFT和FFT之间的关系
  16. 查看mysql版本的方法
  17. 取字符串拼音首字母(js)
  18. 2022淘宝天猫京东双十一交易额有多少?双11交易的数据
  19. python爬虫爬微信数据可信吗_我用 Python 爬取微信好友,最后发现一个大秘密
  20. 分享---蔡康永情商课201集全

热门文章

  1. TMS570ls3137 之FEE使用
  2. 【蓝桥杯】右直角三角形
  3. 用MFC创建个性的对话框
  4. ArcGIS基础实验操作100例--实验3旋转矢量要素
  5. BN层的解释说明(包含梯度消失和梯度爆炸的原理及解决方法)
  6. L3-第五章-动态规划-2657 二进制数字
  7. Leetcode_584. 寻找用户推荐人
  8. 个人简历——各类奖学金、各种称号、各种职位中英文对照:
  9. 2021年熔化焊接与热切割考试题及熔化焊接与热切割报名考试
  10. HDU 1713相遇周期(两个分数的lcm)