Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit d073d325 authored by Ahmad Khalil's avatar Ahmad Khalil
Browse files

Create haptic vibration library

Add an api to set RingtoneManager media type, and then use this type to determine whether the cursor will return Sound or Vibration items.

Bug: 273903859
Test: atest RingtoneManagerTest
Change-Id: I5a1cc0355fc52d738b6ae266846410556f1f2f1e
parent 8c5aa74b
Loading
Loading
Loading
Loading
+11 −0
Original line number Diff line number Diff line
@@ -37,6 +37,17 @@
        }
      ],
      "file_patterns": ["(?i)drm|crypto"]
    },
    {
      "file_patterns": [
        "[^/]*(Ringtone)[^/]*\\.java"
      ],
      "name": "MediaRingtoneTests",
      "options": [
        {"exclude-annotation": "androidx.test.filters.LargeTest"},
        {"exclude-annotation": "androidx.test.filters.FlakyTest"},
        {"exclude-annotation": "org.junit.Ignore"}
      ]
    }
  ]
}
+140 −50
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@

package android.media;

import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
@@ -35,19 +36,20 @@ import android.content.res.AssetFileDescriptor;
import android.database.Cursor;
import android.database.StaleDataException;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.os.FileUtils;
import android.os.ParcelFileDescriptor;
import android.os.SystemProperties;
import android.os.UserHandle;
import android.os.UserManager;
import android.os.vibrator.persistence.VibrationXmlParser;
import android.provider.BaseColumns;
import android.provider.MediaStore;
import android.provider.MediaStore.Audio.AudioColumns;
import android.provider.MediaStore.MediaColumns;
import android.provider.Settings;
import android.provider.Settings.System;
import android.text.TextUtils;
import android.util.Log;

import com.android.internal.database.SortCursor;
@@ -58,6 +60,8 @@ import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.List;

@@ -210,20 +214,29 @@ public class RingtoneManager {
    public static final String EXTRA_RINGTONE_PICKED_URI =
            "android.intent.extra.ringtone.PICKED_URI";

    /**
     * Declares the allowed types of media for this RingtoneManager.
     * @hide
     */
    @Retention(RetentionPolicy.SOURCE)
    @IntDef(prefix = "MEDIA_", value = {
            Ringtone.MEDIA_SOUND,
            Ringtone.MEDIA_VIBRATION,
    })
    public @interface MediaType {}

    // Make sure the column ordering and then ..._COLUMN_INDEX are in sync
    
    private static final String[] INTERNAL_COLUMNS = new String[] {
    private static final String[] MEDIA_AUDIO_COLUMNS = new String[] {
        MediaStore.Audio.Media._ID,
        MediaStore.Audio.Media.TITLE,
        MediaStore.Audio.Media.TITLE,
        MediaStore.Audio.Media.TITLE_KEY,
    };

    private static final String[] MEDIA_COLUMNS = new String[] {
        MediaStore.Audio.Media._ID,
        MediaStore.Audio.Media.TITLE,
        MediaStore.Audio.Media.TITLE,
        MediaStore.Audio.Media.TITLE_KEY,
    private static final String[] MEDIA_VIBRATION_COLUMNS = new String[]{
            MediaStore.Files.FileColumns._ID,
            MediaStore.Files.FileColumns.TITLE,
    };

    /**
@@ -251,6 +264,8 @@ public class RingtoneManager {
    private Cursor mCursor;

    private int mType = TYPE_RINGTONE;
    @MediaType
    private int mMediaType = Ringtone.MEDIA_SOUND;

    /**
     * If a column (item from this list) exists in the Cursor, its value must
@@ -317,6 +332,41 @@ public class RingtoneManager {
        mIncludeParentRingtones = includeParentRingtones;
    }

    /**
     * Sets the media type that will be listed by the RingtoneManager.
     *
     * <p>This method should be called before calling {@link RingtoneManager#getCursor()}.
     *
     * @hide
     */
    public void setMediaType(@MediaType int mediaType) {
        if (mCursor != null) {
            throw new IllegalStateException(
                    "Setting media should be done before calling getCursor().");
        }

        switch (mediaType) {
            case Ringtone.MEDIA_SOUND:
            case Ringtone.MEDIA_VIBRATION:
                mMediaType = mediaType;
                break;
            default:
                throw new IllegalArgumentException("Unsupported media type " + mediaType);
        }
    }

    /**
     * Returns the RingtoneManagers media type.
     *
     * @return the media type.
     * @see #setMediaType
     * @hide
     */
    @MediaType
    public int getMediaType() {
        return mMediaType;
    }

    /**
     * Sets which type(s) of ringtones will be listed by this.
     * 
@@ -454,19 +504,19 @@ public class RingtoneManager {
            return mCursor;
        }

        ArrayList<Cursor> ringtoneCursors = new ArrayList<Cursor>();
        ringtoneCursors.add(getInternalRingtones());
        ringtoneCursors.add(getMediaRingtones());
        ArrayList<Cursor> cursors = new ArrayList<>();

        cursors.add(queryMediaStore(/* internal= */ true));
        cursors.add(queryMediaStore(/* internal= */ false));

        if (mIncludeParentRingtones) {
            Cursor parentRingtonesCursor = getParentProfileRingtones();
            if (parentRingtonesCursor != null) {
                ringtoneCursors.add(parentRingtonesCursor);
                cursors.add(parentRingtonesCursor);
            }
        }

        return mCursor = new SortCursor(ringtoneCursors.toArray(new Cursor[ringtoneCursors.size()]),
                MediaStore.Audio.Media.DEFAULT_SORT_ORDER);
        return mCursor = new SortCursor(cursors.toArray(new Cursor[cursors.size()]),
                getSortOrderForMedia(mMediaType));
    }

    private Cursor getParentProfileRingtones() {
@@ -478,9 +528,7 @@ public class RingtoneManager {
                // We don't need to re-add the internal ringtones for the work profile since
                // they are the same as the personal profile. We just need the external
                // ringtones.
                final Cursor res = getMediaRingtones(parentContext);
                return new ExternalRingtonesCursorWrapper(res, ContentProvider.maybeAddUserId(
                        MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, parentInfo.id));
                return queryMediaStore(parentContext, /* internal= */ false);
            }
        }
        return null;
@@ -502,7 +550,7 @@ public class RingtoneManager {
        Uri positionUri = getRingtoneUri(position);
        if (Ringtone.useRingtoneV2()) {
            mPreviousRingtone = new Ringtone.Builder(
                    mContext, Ringtone.MEDIA_SOUND, getDefaultAudioAttributes(mType))
                    mContext, mMediaType, getDefaultAudioAttributes(mType))
                    .setUri(positionUri)
                    .build();
        } else {
@@ -676,10 +724,12 @@ public class RingtoneManager {
    public static Uri getValidRingtoneUri(Context context) {
        final RingtoneManager rm = new RingtoneManager(context);

        Uri uri = getValidRingtoneUriFromCursorAndClose(context, rm.getInternalRingtones());
        Uri uri = getValidRingtoneUriFromCursorAndClose(context,
                rm.queryMediaStore(/* internal= */ true));

        if (uri == null) {
            uri = getValidRingtoneUriFromCursorAndClose(context, rm.getMediaRingtones());
            uri = getValidRingtoneUriFromCursorAndClose(context,
                    rm.queryMediaStore(/* internal= */ false));
        }
        
        return uri;
@@ -700,28 +750,26 @@ public class RingtoneManager {
        }
    }

    @UnsupportedAppUsage
    private Cursor getInternalRingtones() {
        final Cursor res = query(
                MediaStore.Audio.Media.INTERNAL_CONTENT_URI, INTERNAL_COLUMNS,
                constructBooleanTrueWhereClause(mFilterColumns),
                null, MediaStore.Audio.Media.DEFAULT_SORT_ORDER);
        return new ExternalRingtonesCursorWrapper(res, MediaStore.Audio.Media.INTERNAL_CONTENT_URI);
    private Cursor queryMediaStore(boolean internal) {
        return queryMediaStore(mContext, internal);
    }

    private Cursor getMediaRingtones() {
        final Cursor res = getMediaRingtones(mContext);
        return new ExternalRingtonesCursorWrapper(res, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI);
    private Cursor queryMediaStore(Context context, boolean internal) {
        Uri contentUri = getContentUriForMedia(mMediaType, internal);
        String[] columns =
                mMediaType == Ringtone.MEDIA_VIBRATION ? MEDIA_VIBRATION_COLUMNS
                        : MEDIA_AUDIO_COLUMNS;
        String whereClause = getWhereClauseForMedia(mMediaType, mFilterColumns);
        String sortOrder = getSortOrderForMedia(mMediaType);

        Cursor cursor = query(contentUri, columns, whereClause, /* selectionArgs= */ null,
                sortOrder, context);

        if (context.getUserId() != mContext.getUserId()) {
            contentUri = ContentProvider.maybeAddUserId(contentUri, context.getUserId());
        }

    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
    private Cursor getMediaRingtones(Context context) {
        // MediaStore now returns ringtones on other storage devices, even when
        // we don't have storage or audio permissions
        return query(
                MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, MEDIA_COLUMNS,
                constructBooleanTrueWhereClause(mFilterColumns), null,
                MediaStore.Audio.Media.DEFAULT_SORT_ORDER, context);
        return new ExternalRingtonesCursorWrapper(cursor, contentUri);
    }

    private void setFilterColumnsList(int type) {
@@ -741,6 +789,56 @@ public class RingtoneManager {
        }
    }

    /**
     * Returns the sort order for the specified media.
     *
     * @param media The RingtoneManager media type.
     * @return The sort order column.
     */
    private static String getSortOrderForMedia(@MediaType int media) {
        return media == Ringtone.MEDIA_VIBRATION ? MediaStore.Files.FileColumns.TITLE
                : MediaStore.Audio.Media.DEFAULT_SORT_ORDER;
    }

    /**
     * Returns the content URI based on the specified media and whether it's internal or external
     * storage.
     *
     * @param media    The RingtoneManager media type.
     * @param internal Whether it's for internal or external storage.
     * @return The media content URI.
     */
    private static Uri getContentUriForMedia(@MediaType int media, boolean internal) {
        switch (media) {
            case Ringtone.MEDIA_VIBRATION:
                return MediaStore.Files.getContentUri(
                        internal ? MediaStore.VOLUME_INTERNAL : MediaStore.VOLUME_EXTERNAL);
            case Ringtone.MEDIA_SOUND:
                return internal ? MediaStore.Audio.Media.INTERNAL_CONTENT_URI
                        : MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
            default:
                throw new IllegalArgumentException("Unsupported media type " + media);
        }
    }

    /**
     * Constructs a where clause based on the media type. This will be used to find all matching
     * sound or vibration files.
     *
     * @param media   The RingtoneManager media type.
     * @param columns The columns that must be true, when media type is {@link Ringtone#MEDIA_SOUND}
     * @return The where clause.
     */
    private static String getWhereClauseForMedia(@MediaType int media, List<String> columns) {
        // TODO(b/296213309): Filtering by ringtone-type isn't supported yet for vibrations.
        if (media == Ringtone.MEDIA_VIBRATION) {
            return TextUtils.formatSimple("(%s='%s')", MediaStore.Files.FileColumns.MIME_TYPE,
                    VibrationXmlParser.APPLICATION_VIBRATION_XML_MIME_TYPE);
        }

        return constructBooleanTrueWhereClause(columns);
    }
    
    /**
     * Constructs a where clause that consists of at least one column being 1
     * (true). This is used to find all matching sounds for the given sound
@@ -770,14 +868,6 @@ public class RingtoneManager {
        return sb.toString();
    }

    private Cursor query(Uri uri,
            String[] projection,
            String selection,
            String[] selectionArgs,
            String sortOrder) {
        return query(uri, projection, selection, selectionArgs, sortOrder, mContext);
    }

    private Cursor query(Uri uri,
            String[] projection,
            String selection,
+30 −0
Original line number Diff line number Diff line
package {
    // See: http://go/android-license-faq
    default_applicable_licenses: ["frameworks_base_license"],
}

android_test {
    name: "MediaRingtoneTests",

    srcs: ["src/**/*.java"],

    libs: [
        "android.test.runner",
        "android.test.base",
    ],

    static_libs: [
        "androidx.test.rules",
        "testng",
        "androidx.test.ext.truth",
        "frameworks-base-testutils",
    ],

    test_suites: [
        "device-tests",
        "automotive-tests",
    ],

    platform_apis: true,
    certificate: "platform",
}
+41 −0
Original line number Diff line number Diff line
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2023 The Android Open Source Project

     Licensed under the Apache License, Version 2.0 (the "License");
     you may not use this file except in compliance with the License.
     You may obtain a copy of the License at

          http://www.apache.org/licenses/LICENSE-2.0

     Unless required by applicable law or agreed to in writing, software
     distributed under the License is distributed on an "AS IS" BASIS,
     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     See the License for the specific language governing permissions and
     limitations under the License.
-->

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="com.android.framework.base.media.ringtone.tests">

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

    <application android:debuggable="true">
        <uses-library android:name="android.test.runner" />

        <activity android:name="MediaRingtoneTests"
                  android:label="Media Ringtone Tests"
                  android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>

    </application>

    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
                     android:targetPackage="com.android.framework.base.media.ringtone.tests"
                     android:label="Media Ringtone Tests"/>
</manifest>
+20 −0
Original line number Diff line number Diff line
{
  "presubmit": [
    {
      "name": "MediaRingtoneTests",
      "options": [
        {"exclude-annotation": "androidx.test.filters.LargeTest"},
        {"exclude-annotation": "androidx.test.filters.FlakyTest"},
        {"exclude-annotation": "org.junit.Ignore"}
      ]
    }
  ],
  "postsubmit": [
    {
      "name": "MediaRingtoneTests",
      "options": [
        {"exclude-annotation": "org.junit.Ignore"}
      ]
    }
  ]
}
 No newline at end of file
Loading