diff options
Diffstat (limited to 'common/src/com/android')
63 files changed, 3743 insertions, 715 deletions
diff --git a/common/src/com/android/tv/common/BaseApplication.java b/common/src/com/android/tv/common/BaseApplication.java new file mode 100644 index 00000000..71c9b4d7 --- /dev/null +++ b/common/src/com/android/tv/common/BaseApplication.java @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2017 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. + */ + +package com.android.tv.common; + +import android.annotation.TargetApi; +import android.app.Application; +import android.content.Context; +import android.content.Intent; +import android.os.AsyncTask; +import android.os.Build; +import android.os.StrictMode; +import android.support.annotation.VisibleForTesting; +import com.android.tv.common.feature.CommonFeatures; +import com.android.tv.common.recording.RecordingStorageStatusManager; +import com.android.tv.common.util.Clock; +import com.android.tv.common.util.CommonUtils; +import com.android.tv.common.util.Debug; +import com.android.tv.common.util.SystemProperties; + +/** The base application class for Live TV applications. */ +public abstract class BaseApplication extends Application implements BaseSingletons { + private RecordingStorageStatusManager mRecordingStorageStatusManager; + + /** + * An instance of {@link BaseSingletons}. Note that this can be set directly only for the test + * purpose. + */ + @VisibleForTesting public static BaseSingletons sSingletons; + + /** Returns the {@link BaseSingletons} using the application context. */ + public static BaseSingletons getSingletons(Context context) { + // STOP-SHIP: changing the method to protected once the Tuner application is created. + // No need to be "synchronized" because this doesn't create any instance. + if (sSingletons == null) { + sSingletons = (BaseSingletons) context.getApplicationContext(); + } + return sSingletons; + } + + @Override + public void onCreate() { + super.onCreate(); + Debug.getTimer(Debug.TAG_START_UP_TIMER).start(); + Debug.getTimer(Debug.TAG_START_UP_TIMER) + .log("Start " + this.getClass().getSimpleName() + ".onCreate"); + CommonPreferences.initialize(this); + + // Only set StrictMode for ENG builds because the build server only produces userdebug + // builds. + if (BuildConfig.ENG && SystemProperties.ALLOW_STRICT_MODE.getValue()) { + StrictMode.ThreadPolicy.Builder threadPolicyBuilder = + new StrictMode.ThreadPolicy.Builder().detectAll().penaltyLog(); + // TODO(b/69565157): Turn penaltyDeath on for VMPolicy when tests are fixed. + StrictMode.VmPolicy.Builder vmPolicyBuilder = + new StrictMode.VmPolicy.Builder().detectAll().penaltyLog(); + + if (!CommonUtils.isRunningInTest()) { + threadPolicyBuilder.penaltyDialog(); + } + StrictMode.setThreadPolicy(threadPolicyBuilder.build()); + StrictMode.setVmPolicy(vmPolicyBuilder.build()); + } + if (CommonFeatures.DVR.isEnabled(this)) { + getRecordingStorageStatusManager(); + } + new AsyncTask<Void, Void, Void>() { + @Override + protected Void doInBackground(Void... params) { + // Fetch remote config + getRemoteConfig().fetch(null); + return null; + } + }.execute(); + } + + @Override + public Clock getClock() { + return Clock.SYSTEM; + } + + /** Returns {@link RecordingStorageStatusManager}. */ + @Override + @TargetApi(Build.VERSION_CODES.N) + public RecordingStorageStatusManager getRecordingStorageStatusManager() { + if (mRecordingStorageStatusManager == null) { + mRecordingStorageStatusManager = new RecordingStorageStatusManager(this); + } + return mRecordingStorageStatusManager; + } + + @Override + public abstract Intent getTunerSetupIntent(Context context); +} diff --git a/common/src/com/android/tv/common/BaseSingletons.java b/common/src/com/android/tv/common/BaseSingletons.java new file mode 100644 index 00000000..e735cdb4 --- /dev/null +++ b/common/src/com/android/tv/common/BaseSingletons.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2017 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. + */ + +package com.android.tv.common; + +import android.content.Context; +import android.content.Intent; +import com.android.tv.common.config.api.RemoteConfig.HasRemoteConfig; +import com.android.tv.common.recording.RecordingStorageStatusManager; +import com.android.tv.common.util.Clock; + +/** Injection point for the base app */ +public interface BaseSingletons extends HasRemoteConfig { + + Clock getClock(); + + RecordingStorageStatusManager getRecordingStorageStatusManager(); + + Intent getTunerSetupIntent(Context context); + + String getEmbeddedTunerInputId(); +} diff --git a/common/src/com/android/tv/common/BooleanSystemProperty.java b/common/src/com/android/tv/common/BooleanSystemProperty.java index 21e575a1..5436524f 100644 --- a/common/src/com/android/tv/common/BooleanSystemProperty.java +++ b/common/src/com/android/tv/common/BooleanSystemProperty.java @@ -17,7 +17,6 @@ package com.android.tv.common; import android.util.Log; - import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; @@ -25,16 +24,15 @@ import java.util.List; /** * Lazy loaded boolean system property. * - * <p>Set with <code>adb shell setprop <em>key</em> <em>value</em></code> where: - * Values 'n', 'no', '0', 'false' or 'off' are considered false. - * Values 'y', 'yes', '1', 'true' or 'on' are considered true. - * (case sensitive). See <a href= + * <p>Set with <code>adb shell setprop <em>key</em> <em>value</em></code> where: Values 'n', 'no', + * '0', 'false' or 'off' are considered false. Values 'y', 'yes', '1', 'true' or 'on' are considered + * true. (case sensitive). See <a href= * "https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/os/SystemProperties.java" * >android.os.SystemProperties.getBoolean</a>. */ public class BooleanSystemProperty { - private final static String TAG = "BooleanSystemProperty"; - private final static boolean DEBUG = false; + private static final String TAG = "BooleanSystemProperty"; + private static final boolean DEBUG = false; private static final List<BooleanSystemProperty> ALL_PROPERTIES = new ArrayList<>(); private final boolean mDefaultValue; private final String mKey; @@ -78,7 +76,7 @@ public class BooleanSystemProperty { } /** - * Clears the cached value. The next call to getValue will check {@code + * Clears the cached value. The next call to getValue will check {@code * android.os.SystemProperties}. */ public void reset() { @@ -88,7 +86,7 @@ public class BooleanSystemProperty { /** * Returns the value of the system property. * - * <p>If the value is cached get the value from {@code android.os.SystemProperties} with the + * <p>If the value is cached get the value from {@code android.os.SystemProperties} with the * default set in the constructor. */ public boolean getValue() { diff --git a/common/src/com/android/tv/common/CommonConstants.java b/common/src/com/android/tv/common/CommonConstants.java new file mode 100644 index 00000000..ac379d18 --- /dev/null +++ b/common/src/com/android/tv/common/CommonConstants.java @@ -0,0 +1,28 @@ +/* + * Copyright 2015 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. + */ + +package com.android.tv.common; + +/** Constants for common use in apps and tests. */ +public final class CommonConstants { + + public static final String BASE_PACKAGE = + "com.android.tv"; + /** A constant for the key of the extra data for the app linking intent. */ + public static final String EXTRA_APP_LINK_CHANNEL_URI = "app_link_channel_uri"; + + private CommonConstants() {} +} diff --git a/common/src/com/android/tv/common/CommonPreferenceProvider.java b/common/src/com/android/tv/common/CommonPreferenceProvider.java new file mode 100644 index 00000000..b0be3038 --- /dev/null +++ b/common/src/com/android/tv/common/CommonPreferenceProvider.java @@ -0,0 +1,215 @@ +/* + * Copyright (C) 2015 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. + */ + +package com.android.tv.common; + +import android.content.ContentProvider; +import android.content.ContentValues; +import android.content.Context; +import android.content.UriMatcher; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteException; +import android.database.sqlite.SQLiteOpenHelper; +import android.net.Uri; + +/** + * A content provider for storing common preferences. It's used across TV app and tuner TV inputs. + */ +public class CommonPreferenceProvider extends ContentProvider { + /** The authority of the provider */ + public static final String AUTHORITY = CommonConstants.BASE_PACKAGE + ".common.preferences"; + + private static final String PATH_PREFERENCES = "preferences"; + + private static final int DATABASE_VERSION = 1; + private static final String DATABASE_NAME = "usbtuner_preferences.db"; + private static final String PREFERENCES_TABLE = "preferences"; + + private static final int MATCH_PREFERENCE = 1; + private static final int MATCH_PREFERENCE_KEY = 2; + + private static final UriMatcher sUriMatcher; + + private DatabaseOpenHelper mDatabaseOpenHelper; + + static { + sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH); + sUriMatcher.addURI(AUTHORITY, "preferences", MATCH_PREFERENCE); + sUriMatcher.addURI(AUTHORITY, "preferences/*", MATCH_PREFERENCE_KEY); + } + + /** + * Builds a Uri that points to a specific preference. + * + * @param key a key of the preference to point to + */ + public static Uri buildPreferenceUri(String key) { + return Preferences.CONTENT_URI.buildUpon().appendPath(key).build(); + } + + /** Columns definitions for the preferences table. */ + public interface Preferences { + + /** The content:// style for the preferences table. */ + Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/" + PATH_PREFERENCES); + + /** The MIME type of a directory of preferences. */ + String CONTENT_TYPE = "vnd.android.cursor.dir/preferences"; + + /** The MIME type of a single preference. */ + String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/preferences"; + + /** + * The ID of this preference. + * + * <p>This is auto-incremented. + * + * <p>Type: INTEGER + */ + String _ID = "_id"; + + /** + * The key of this preference. + * + * <p>Should be unique. + * + * <p>Type: TEXT + */ + String COLUMN_KEY = "key"; + + /** + * The value of this preference. + * + * <p>Type: TEXT + */ + String COLUMN_VALUE = "value"; + } + + private static class DatabaseOpenHelper extends SQLiteOpenHelper { + public DatabaseOpenHelper(Context context) { + super(context, DATABASE_NAME, null, DATABASE_VERSION); + } + + @Override + public void onCreate(SQLiteDatabase db) { + db.execSQL( + "CREATE TABLE " + + PREFERENCES_TABLE + + " (" + + Preferences._ID + + " INTEGER PRIMARY KEY AUTOINCREMENT," + + Preferences.COLUMN_KEY + + " TEXT NOT NULL," + + Preferences.COLUMN_VALUE + + " TEXT);"); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + // No-op + } + } + + @Override + public boolean onCreate() { + mDatabaseOpenHelper = new DatabaseOpenHelper(getContext()); + return true; + } + + @Override + public Cursor query( + Uri uri, + String[] projection, + String selection, + String[] selectionArgs, + String sortOrder) { + int match = sUriMatcher.match(uri); + if (match != MATCH_PREFERENCE && match != MATCH_PREFERENCE_KEY) { + throw new UnsupportedOperationException(); + } + SQLiteDatabase db = mDatabaseOpenHelper.getReadableDatabase(); + Cursor cursor = + db.query( + PREFERENCES_TABLE, + projection, + selection, + selectionArgs, + null, + null, + sortOrder); + cursor.setNotificationUri(getContext().getContentResolver(), uri); + return cursor; + } + + @Override + public String getType(Uri uri) { + switch (sUriMatcher.match(uri)) { + case MATCH_PREFERENCE: + return Preferences.CONTENT_TYPE; + case MATCH_PREFERENCE_KEY: + return Preferences.CONTENT_ITEM_TYPE; + default: + throw new IllegalArgumentException("Unknown URI " + uri); + } + } + + /** + * Inserts a preference row into the preference table. + * + * <p>If a key is already exists in the table, it removes the old row and inserts a new row. + * + * @param uri the URL of the table to insert into + * @param values the initial values for the newly inserted row + * @return the URL of the newly created row + */ + @Override + public Uri insert(Uri uri, ContentValues values) { + if (sUriMatcher.match(uri) != MATCH_PREFERENCE) { + throw new UnsupportedOperationException(); + } + return insertRow(uri, values); + } + + private Uri insertRow(Uri uri, ContentValues values) { + SQLiteDatabase db = mDatabaseOpenHelper.getWritableDatabase(); + + // Remove the old row. + db.delete( + PREFERENCES_TABLE, + Preferences.COLUMN_KEY + " like ?", + new String[] {values.getAsString(Preferences.COLUMN_KEY)}); + + long rowId = db.insert(PREFERENCES_TABLE, null, values); + if (rowId > 0) { + Uri rowUri = buildPreferenceUri(values.getAsString(Preferences.COLUMN_KEY)); + getContext().getContentResolver().notifyChange(rowUri, null); + return rowUri; + } + + throw new SQLiteException("Failed to insert row into " + uri); + } + + @Override + public int delete(Uri uri, String selection, String[] selectionArgs) { + throw new UnsupportedOperationException(); + } + + @Override + public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { + throw new UnsupportedOperationException(); + } +} diff --git a/common/src/com/android/tv/common/CommonPreferences.java b/common/src/com/android/tv/common/CommonPreferences.java new file mode 100644 index 00000000..5a94eecb --- /dev/null +++ b/common/src/com/android/tv/common/CommonPreferences.java @@ -0,0 +1,332 @@ +/* + * Copyright (C) 2017 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. + */ +package com.android.tv.common; + +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.content.SharedPreferences; +import android.database.ContentObserver; +import android.database.Cursor; +import android.os.AsyncTask; +import android.os.Bundle; +import android.os.Handler; +import android.support.annotation.GuardedBy; +import android.support.annotation.IntDef; +import android.support.annotation.MainThread; +import android.util.Log; +import com.android.tv.common.CommonPreferenceProvider.Preferences; +import com.android.tv.common.util.CommonUtils; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.HashMap; +import java.util.Map; + +/** A helper class for setting/getting common preferences across applications. */ +public class CommonPreferences { + private static final String TAG = "CommonPreferences"; + + private static final String PREFS_KEY_LAUNCH_SETUP = "launch_setup"; + private static final String PREFS_KEY_STORE_TS_STREAM = "store_ts_stream"; + private static final String PREFS_KEY_TRICKPLAY_SETTING = "trickplay_setting"; + private static final String PREFS_KEY_LAST_POSTAL_CODE = "last_postal_code"; + + private static final Map<String, Class> sPref2TypeMapping = new HashMap<>(); + + static { + sPref2TypeMapping.put(PREFS_KEY_TRICKPLAY_SETTING, int.class); + sPref2TypeMapping.put(PREFS_KEY_STORE_TS_STREAM, boolean.class); + sPref2TypeMapping.put(PREFS_KEY_LAUNCH_SETUP, boolean.class); + sPref2TypeMapping.put(PREFS_KEY_LAST_POSTAL_CODE, String.class); + } + + private static final String SHARED_PREFS_NAME = + CommonConstants.BASE_PACKAGE + ".common.preferences"; + + @IntDef({TRICKPLAY_SETTING_NOT_SET, TRICKPLAY_SETTING_DISABLED, TRICKPLAY_SETTING_ENABLED}) + @Retention(RetentionPolicy.SOURCE) + public @interface TrickplaySetting {} + + /** Trickplay setting is not changed by a user. Trickplay will be enabled in this case. */ + public static final int TRICKPLAY_SETTING_NOT_SET = -1; + + /** Trickplay setting is disabled. */ + public static final int TRICKPLAY_SETTING_DISABLED = 0; + + /** Trickplay setting is enabled. */ + public static final int TRICKPLAY_SETTING_ENABLED = 1; + + @GuardedBy("CommonPreferences.class") + private static final Bundle sPreferenceValues = new Bundle(); + + private static LoadPreferencesTask sLoadPreferencesTask; + private static ContentObserver sContentObserver; + private static CommonPreferencesChangedListener sPreferencesChangedListener = null; + + protected static boolean sInitialized; + + /** Listeners for CommonPreferences change. */ + public interface CommonPreferencesChangedListener { + void onCommonPreferencesChanged(); + } + + /** Initializes the common preferences. */ + @MainThread + public static void initialize(final Context context) { + if (sInitialized) { + return; + } + sInitialized = true; + if (useContentProvider(context)) { + loadPreferences(context); + sContentObserver = + new ContentObserver(new Handler()) { + @Override + public void onChange(boolean selfChange) { + loadPreferences(context); + } + }; + context.getContentResolver() + .registerContentObserver(Preferences.CONTENT_URI, true, sContentObserver); + } else { + new AsyncTask<Void, Void, Void>() { + @Override + protected Void doInBackground(Void... params) { + getSharedPreferences(context); + return null; + } + }.execute(); + } + } + + /** Releases the resources. */ + public static synchronized void release(Context context) { + if (useContentProvider(context) && sContentObserver != null) { + context.getContentResolver().unregisterContentObserver(sContentObserver); + } + setCommonPreferencesChangedListener(null); + } + + /** Sets the listener for CommonPreferences change. */ + public static void setCommonPreferencesChangedListener( + CommonPreferencesChangedListener listener) { + sPreferencesChangedListener = listener; + } + + /** + * Loads the preferences from database. + * + * <p>This preferences is used across processes, so the preferences should be loaded again when + * the databases changes. + */ + @MainThread + public static void loadPreferences(Context context) { + if (sLoadPreferencesTask != null + && sLoadPreferencesTask.getStatus() != AsyncTask.Status.FINISHED) { + sLoadPreferencesTask.cancel(true); + } + sLoadPreferencesTask = new LoadPreferencesTask(context); + sLoadPreferencesTask.execute(); + } + + private static boolean useContentProvider(Context context) { + // If TIS is a part of LC, it should use ContentProvider to resolve multiple process access. + return CommonUtils.isPackagedWithLiveChannels(context); + } + + public static synchronized boolean shouldShowSetupActivity(Context context) { + SoftPreconditions.checkState(sInitialized); + if (useContentProvider(context)) { + return sPreferenceValues.getBoolean(PREFS_KEY_LAUNCH_SETUP); + } else { + return getSharedPreferences(context).getBoolean(PREFS_KEY_LAUNCH_SETUP, false); + } + } + + public static synchronized void setShouldShowSetupActivity(Context context, boolean need) { + if (useContentProvider(context)) { + setPreference(context, PREFS_KEY_LAUNCH_SETUP, need); + } else { + getSharedPreferences(context).edit().putBoolean(PREFS_KEY_LAUNCH_SETUP, need).apply(); + } + } + + public static synchronized @TrickplaySetting int getTrickplaySetting(Context context) { + SoftPreconditions.checkState(sInitialized); + if (useContentProvider(context)) { + return sPreferenceValues.getInt(PREFS_KEY_TRICKPLAY_SETTING, TRICKPLAY_SETTING_NOT_SET); + } else { + return getSharedPreferences(context) + .getInt(PREFS_KEY_TRICKPLAY_SETTING, TRICKPLAY_SETTING_NOT_SET); + } + } + + public static synchronized void setTrickplaySetting( + Context context, @TrickplaySetting int trickplaySetting) { + SoftPreconditions.checkState(sInitialized); + SoftPreconditions.checkArgument(trickplaySetting != TRICKPLAY_SETTING_NOT_SET); + if (useContentProvider(context)) { + setPreference(context, PREFS_KEY_TRICKPLAY_SETTING, trickplaySetting); + } else { + getSharedPreferences(context) + .edit() + .putInt(PREFS_KEY_TRICKPLAY_SETTING, trickplaySetting) + .apply(); + } + } + + public static synchronized boolean getStoreTsStream(Context context) { + SoftPreconditions.checkState(sInitialized); + if (useContentProvider(context)) { + return sPreferenceValues.getBoolean(PREFS_KEY_STORE_TS_STREAM, false); + } else { + return getSharedPreferences(context).getBoolean(PREFS_KEY_STORE_TS_STREAM, false); + } + } + + public static synchronized void setStoreTsStream(Context context, boolean shouldStore) { + if (useContentProvider(context)) { + setPreference(context, PREFS_KEY_STORE_TS_STREAM, shouldStore); + } else { + getSharedPreferences(context) + .edit() + .putBoolean(PREFS_KEY_STORE_TS_STREAM, shouldStore) + .apply(); + } + } + + public static synchronized String getLastPostalCode(Context context) { + SoftPreconditions.checkState(sInitialized); + if (useContentProvider(context)) { + return sPreferenceValues.getString(PREFS_KEY_LAST_POSTAL_CODE); + } else { + return getSharedPreferences(context).getString(PREFS_KEY_LAST_POSTAL_CODE, null); + } + } + + public static synchronized void setLastPostalCode(Context context, String postalCode) { + if (useContentProvider(context)) { + setPreference(context, PREFS_KEY_LAST_POSTAL_CODE, postalCode); + } else { + getSharedPreferences(context) + .edit() + .putString(PREFS_KEY_LAST_POSTAL_CODE, postalCode) + .apply(); + } + } + + protected static SharedPreferences getSharedPreferences(Context context) { + return context.getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE); + } + + private static synchronized void setPreference(Context context, String key, String value) { + sPreferenceValues.putString(key, value); + savePreference(context, key, value); + } + + private static synchronized void setPreference(Context context, String key, int value) { + sPreferenceValues.putInt(key, value); + savePreference(context, key, Integer.toString(value)); + } + + private static synchronized void setPreference(Context context, String key, long value) { + sPreferenceValues.putLong(key, value); + savePreference(context, key, Long.toString(value)); + } + + private static synchronized void setPreference(Context context, String key, boolean value) { + sPreferenceValues.putBoolean(key, value); + savePreference(context, key, Boolean.toString(value)); + } + + private static void savePreference( + final Context context, final String key, final String value) { + new AsyncTask<Void, Void, Void>() { + @Override + protected Void doInBackground(Void... params) { + ContentResolver resolver = context.getContentResolver(); + ContentValues values = new ContentValues(); + values.put(Preferences.COLUMN_KEY, key); + values.put(Preferences.COLUMN_VALUE, value); + try { + resolver.insert(Preferences.CONTENT_URI, values); + } catch (Exception e) { + SoftPreconditions.warn( + TAG, "setPreference", e, "Error writing preference values"); + } + return null; + } + }.execute(); + } + + private static class LoadPreferencesTask extends AsyncTask<Void, Void, Bundle> { + private final Context mContext; + + private LoadPreferencesTask(Context context) { + mContext = context; + } + + @Override + protected Bundle doInBackground(Void... params) { + Bundle bundle = new Bundle(); + ContentResolver resolver = mContext.getContentResolver(); + String[] projection = new String[] {Preferences.COLUMN_KEY, Preferences.COLUMN_VALUE}; + try (Cursor cursor = + resolver.query(Preferences.CONTENT_URI, projection, null, null, null)) { + if (cursor != null) { + while (!isCancelled() && cursor.moveToNext()) { + String key = cursor.getString(0); + String value = cursor.getString(1); + Class prefClass = sPref2TypeMapping.get(key); + if (prefClass == int.class) { + try { + bundle.putInt(key, Integer.parseInt(value)); + } catch (NumberFormatException e) { + Log.w(TAG, "Invalid format, key=" + key + ", value=" + value); + } + } else if (prefClass == long.class) { + try { + bundle.putLong(key, Long.parseLong(value)); + } catch (NumberFormatException e) { + Log.w(TAG, "Invalid format, key=" + key + ", value=" + value); + } + } else if (prefClass == boolean.class) { + bundle.putBoolean(key, Boolean.parseBoolean(value)); + } else { + bundle.putString(key, value); + } + } + } + } catch (Exception e) { + SoftPreconditions.warn(TAG, "getPreference", e, "Error querying preference values"); + return null; + } + return bundle; + } + + @Override + protected void onPostExecute(Bundle bundle) { + synchronized (CommonPreferences.class) { + if (bundle != null) { + sPreferenceValues.putAll(bundle); + } + } + if (sPreferencesChangedListener != null) { + sPreferencesChangedListener.onCommonPreferencesChanged(); + } + } + } +} diff --git a/common/src/com/android/tv/common/SoftPreconditions.java b/common/src/com/android/tv/common/SoftPreconditions.java index 823c42ff..3b0510d8 100644 --- a/common/src/com/android/tv/common/SoftPreconditions.java +++ b/common/src/com/android/tv/common/SoftPreconditions.java @@ -17,17 +17,18 @@ package com.android.tv.common; import android.content.Context; +import android.support.annotation.Nullable; import android.text.TextUtils; import android.util.Log; - import com.android.tv.common.feature.Feature; +import com.android.tv.common.util.CommonUtils; /** - * Simple static methods to be called at the start of your own methods to verify - * correct arguments and state. + * Simple static methods to be called at the start of your own methods to verify correct arguments + * and state. * - * <p>{@code checkXXX} methods throw exceptions when {@link BuildConfig#ENG} is true, and - * logs a warning when it is false. + * <p>{@code checkXXX} methods throw exceptions when {@link BuildConfig#ENG} is true, and logs a + * warning when it is false. * * <p>This is based on com.android.internal.util.Preconditions. */ @@ -35,26 +36,35 @@ public final class SoftPreconditions { private static final String TAG = "SoftPreconditions"; /** - * Throws or logs if an expression involving the parameter of the calling - * method is not true. + * Throws or logs if an expression involving the parameter of the calling method is not true. * * @param expression a boolean expression - * @param tag Used to identify the source of a log message. It usually - * identifies the class or activity where the log call occurs. - * @param msg The message you would like logged. + * @param tag Used to identify the source of a log message. It usually identifies the class or + * activity where the log call occurs. + * @param errorMessageTemplate a template for the exception message should the check fail. The + * message is formed by replacing each {@code %s} placeholder in the template with an + * argument. These are matched by position - the first {@code %s} gets {@code + * errorMessageArgs[0]}, etc. Unmatched arguments will be appended to the formatted message + * in square braces. Unmatched placeholders will be left as-is. + * @param errorMessageArgs the arguments to be substituted into the message template. Arguments + * are converted to strings using {@link String#valueOf(Object)}. * @return the evaluation result of the boolean expression * @throws IllegalArgumentException if {@code expression} is true */ - public static boolean checkArgument(final boolean expression, String tag, String msg) { + public static boolean checkArgument( + final boolean expression, + String tag, + @Nullable String errorMessageTemplate, + @Nullable Object... errorMessageArgs) { if (!expression) { - warn(tag, "Illegal argument", msg, new IllegalArgumentException(msg)); + String msg = format(errorMessageTemplate, errorMessageArgs); + warn(tag, "Illegal argument", new IllegalArgumentException(msg), msg); } return expression; } /** - * Throws or logs if an expression involving the parameter of the calling - * method is not true. + * Throws or logs if an expression involving the parameter of the calling method is not true. * * @param expression a boolean expression * @return the evaluation result of the boolean expression @@ -69,15 +79,26 @@ public final class SoftPreconditions { * Throws or logs if an and object is null. * * @param reference an object reference - * @param tag Used to identify the source of a log message. It usually - * identifies the class or activity where the log call occurs. - * @param msg The message you would like logged. + * @param tag Used to identify the source of a log message. It usually identifies the class or + * activity where the log call occurs. + * @param errorMessageTemplate a template for the exception message should the check fail. The + * message is formed by replacing each {@code %s} placeholder in the template with an + * argument. These are matched by position - the first {@code %s} gets {@code + * errorMessageArgs[0]}, etc. Unmatched arguments will be appended to the formatted message + * in square braces. Unmatched placeholders will be left as-is. + * @param errorMessageArgs the arguments to be substituted into the message template. Arguments + * are converted to strings using {@link String#valueOf(Object)}. * @return true if the object is null * @throws NullPointerException if {@code reference} is null */ - public static <T> T checkNotNull(final T reference, String tag, String msg) { + public static <T> T checkNotNull( + final T reference, + String tag, + @Nullable String errorMessageTemplate, + @Nullable Object... errorMessageArgs) { if (reference == null) { - warn(tag, "Null Pointer", msg, new NullPointerException(msg)); + String msg = format(errorMessageTemplate, errorMessageArgs); + warn(tag, "Null Pointer", new NullPointerException(msg), msg); } return reference; } @@ -94,26 +115,37 @@ public final class SoftPreconditions { } /** - * Throws or logs if an expression involving the state of the calling - * instance, but not involving any parameters to the calling method is not true. + * Throws or logs if an expression involving the state of the calling instance, but not + * involving any parameters to the calling method is not true. * * @param expression a boolean expression - * @param tag Used to identify the source of a log message. It usually - * identifies the class or activity where the log call occurs. - * @param msg The message you would like logged. + * @param tag Used to identify the source of a log message. It usually identifies the class or + * activity where the log call occurs. + * @param errorMessageTemplate a template for the exception message should the check fail. The + * message is formed by replacing each {@code %s} placeholder in the template with an + * argument. These are matched by position - the first {@code %s} gets {@code + * errorMessageArgs[0]}, etc. Unmatched arguments will be appended to the formatted message + * in square braces. Unmatched placeholders will be left as-is. + * @param errorMessageArgs the arguments to be substituted into the message template. Arguments + * are converted to strings using {@link String#valueOf(Object)}. * @return the evaluation result of the boolean expression * @throws IllegalStateException if {@code expression} is true */ - public static boolean checkState(final boolean expression, String tag, String msg) { + public static boolean checkState( + final boolean expression, + String tag, + @Nullable String errorMessageTemplate, + @Nullable Object... errorMessageArgs) { if (!expression) { - warn(tag, "Illegal State", msg, new IllegalStateException(msg)); + String msg = format(errorMessageTemplate, errorMessageArgs); + warn(tag, "Illegal State", new IllegalStateException(msg), msg); } return expression; } /** - * Throws or logs if an expression involving the state of the calling - * instance, but not involving any parameters to the calling method is not true. + * Throws or logs if an expression involving the state of the calling instance, but not + * involving any parameters to the calling method is not true. * * @param expression a boolean expression * @return the evaluation result of the boolean expression @@ -129,8 +161,8 @@ public final class SoftPreconditions { * * @param context an android context * @param feature the required feature - * @param tag used to identify the source of a log message. It usually - * identifies the class or activity where the log call occurs + * @param tag used to identify the source of a log message. It usually identifies the class or + * activity where the log call occurs * @throws IllegalStateException if {@code feature} is not enabled */ public static void checkFeatureEnabled(Context context, Feature feature, String tag) { @@ -138,14 +170,15 @@ public final class SoftPreconditions { } /** - * Throws a {@link RuntimeException} if {@link BuildConfig#ENG} is true, else log a warning. + * Throws a {@link RuntimeException} if {@link BuildConfig#ENG} is true and not running in a + * test, else log a warning. * - * @param tag Used to identify the source of a log message. It usually - * identifies the class or activity where the log call occurs. - * @param msg The message you would like logged + * @param tag Used to identify the source of a log message. It usually identifies the class or + * activity where the log call occurs. * @param e The exception to wrap with a RuntimeException when thrown. + * @param msg The message to be logged */ - public static void warn(String tag, String prefix, String msg, Exception e) + public static void warn(String tag, String prefix, Exception e, String msg) throws RuntimeException { if (TextUtils.isEmpty(tag)) { tag = TAG; @@ -159,13 +192,57 @@ public final class SoftPreconditions { logMessage = prefix + ": " + msg; } - if (BuildConfig.ENG) { + if (BuildConfig.ENG && !CommonUtils.isRunningInTest()) { throw new RuntimeException(msg, e); } else { Log.w(tag, logMessage, e); } } - private SoftPreconditions() { + /** + * Substitutes each {@code %s} in {@code template} with an argument. These are matched by + * position: the first {@code %s} gets {@code args[0]}, etc. If there are more arguments than + * placeholders, the unmatched arguments will be appended to the end of the formatted message in + * square braces. + * + * @param template a string containing 0 or more {@code %s} placeholders. null is treated as + * "null". + * @param args the arguments to be substituted into the message template. Arguments are + * converted to strings using {@link String#valueOf(Object)}. Arguments can be null. + */ + static String format(@Nullable String template, @Nullable Object... args) { + template = String.valueOf(template); // null -> "null" + + args = args == null ? new Object[] {"(Object[])null"} : args; + + // start substituting the arguments into the '%s' placeholders + StringBuilder builder = new StringBuilder(template.length() + 16 * args.length); + int templateStart = 0; + int i = 0; + while (i < args.length) { + int placeholderStart = template.indexOf("%s", templateStart); + if (placeholderStart == -1) { + break; + } + builder.append(template, templateStart, placeholderStart); + builder.append(args[i++]); + templateStart = placeholderStart + 2; + } + builder.append(template, templateStart, template.length()); + + // if we run out of placeholders, append the extra args in square braces + if (i < args.length) { + builder.append(" ["); + builder.append(args[i++]); + while (i < args.length) { + builder.append(", "); + builder.append(args[i++]); + } + builder.append(']'); + } + + return builder.toString(); } + + private SoftPreconditions() {} } diff --git a/common/src/com/android/tv/common/TvCommonConstants.java b/common/src/com/android/tv/common/TvCommonConstants.java deleted file mode 100644 index 9824497e..00000000 --- a/common/src/com/android/tv/common/TvCommonConstants.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright 2015 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. - */ - -package com.android.tv.common; - -import android.media.tv.TvInputInfo; -import android.os.Build; - -/** - * Constants for common use in TV app and tests. - */ -public final class TvCommonConstants { - /** - * A constant for the key of the extra data for the app linking intent. - */ - public static final String EXTRA_APP_LINK_CHANNEL_URI = "app_link_channel_uri"; - - /** - * An intent action to launch setup activity of a TV input. The intent should include - * TV input ID in the value of {@link EXTRA_INPUT_ID}. Optionally, given the value of - * {@link EXTRA_ACTIVITY_AFTER_COMPLETION}, the activity will be launched after the setup - * activity successfully finishes. - */ - public static final String INTENT_ACTION_INPUT_SETUP = - "com.android.tv.action.LAUNCH_INPUT_SETUP"; - - /** - * A constant of the key to indicate a TV input ID for the intent action - * {@link INTENT_ACTION_INPUT_SETUP}. - * - * <p>Value type: String - */ - public static final String EXTRA_INPUT_ID = TvInputInfo.EXTRA_INPUT_ID; - - /** - * A constant of the key for intent to launch actual TV input setup activity used with - * {@link INTENT_ACTION_INPUT_SETUP}. - * - * <p>Value type: Intent (Parcelable) - */ - public static final String EXTRA_SETUP_INTENT = - "com.android.tv.extra.SETUP_INTENT"; - - /** - * A constant of the key to indicate an Activity launch intent for the intent action - * {@link INTENT_ACTION_INPUT_SETUP}. - * - * <p>Value type: Intent (Parcelable) - */ - public static final String EXTRA_ACTIVITY_AFTER_COMPLETION = - "com.android.tv.intent.extra.ACTIVITY_AFTER_COMPLETION"; - - private TvCommonConstants() { - } -} diff --git a/common/src/com/android/tv/common/TvCommonUtils.java b/common/src/com/android/tv/common/TvCommonUtils.java deleted file mode 100644 index c391ad24..00000000 --- a/common/src/com/android/tv/common/TvCommonUtils.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright 2015 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. - */ - -package com.android.tv.common; - -import android.content.Intent; -import android.media.tv.TvInputInfo; - -/** - * Util class for common use in TV app and inputs. - */ -public final class TvCommonUtils { - private static Boolean sRunningInTest; - - private TvCommonUtils() { } - - /** - * Returns an intent to start the setup activity for the TV input using {@link - * TvCommonConstants#INTENT_ACTION_INPUT_SETUP}. - */ - public static Intent createSetupIntent(Intent originalSetupIntent, String inputId) { - if (originalSetupIntent == null) { - return null; - } - Intent setupIntent = new Intent(originalSetupIntent); - if (!TvCommonConstants.INTENT_ACTION_INPUT_SETUP.equals(originalSetupIntent.getAction())) { - Intent intentContainer = new Intent(TvCommonConstants.INTENT_ACTION_INPUT_SETUP); - intentContainer.putExtra(TvCommonConstants.EXTRA_SETUP_INTENT, originalSetupIntent); - intentContainer.putExtra(TvCommonConstants.EXTRA_INPUT_ID, inputId); - setupIntent = intentContainer; - } - return setupIntent; - } - - /** - * Returns an intent to start the setup activity for this TV input using {@link - * TvCommonConstants#INTENT_ACTION_INPUT_SETUP}. - */ - public static Intent createSetupIntent(TvInputInfo input) { - return createSetupIntent(input.createSetupIntent(), input.getId()); - } - - /** - * Checks if this application is running in tests. - * - * <p>{@link android.app.ActivityManager#isRunningInTestHarness} doesn't return {@code true} for - * the usual devices even the application is running in tests. We need to figure it out by - * checking whether the class in tv-tests-common module can be loaded or not. - */ - public static synchronized boolean isRunningInTest() { - if (sRunningInTest == null) { - try { - Class.forName("com.android.tv.testing.Utils"); - sRunningInTest = true; - } catch (ClassNotFoundException e) { - sRunningInTest = false; - } - } - return sRunningInTest; - } -} diff --git a/common/src/com/android/tv/common/TvContentRatingCache.java b/common/src/com/android/tv/common/TvContentRatingCache.java index 8b3c06f1..cfdb8e4d 100644 --- a/common/src/com/android/tv/common/TvContentRatingCache.java +++ b/common/src/com/android/tv/common/TvContentRatingCache.java @@ -22,7 +22,7 @@ import android.support.annotation.VisibleForTesting; import android.text.TextUtils; import android.util.ArrayMap; import android.util.Log; - +import com.android.tv.common.memory.MemoryManageable; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -31,13 +31,11 @@ import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; -/** - * TvContentRating cache. - */ +/** TvContentRating cache. */ public final class TvContentRatingCache implements MemoryManageable { - private final static String TAG = "TvContentRatings"; + private static final String TAG = "TvContentRatings"; - private final static TvContentRatingCache INSTANCE = new TvContentRatingCache(); + private static final TvContentRatingCache INSTANCE = new TvContentRatingCache(); public static TvContentRatingCache getInstance() { return INSTANCE; @@ -48,8 +46,8 @@ public final class TvContentRatingCache implements MemoryManageable { /** * Returns an array TvContentRatings from a string of comma separated set of rating strings - * creating each from {@link TvContentRating#unflattenFromString(String)} if needed. - * Returns {@code null} if the string is empty or contains no valid ratings. + * creating each from {@link TvContentRating#unflattenFromString(String)} if needed. Returns + * {@code null} if the string is empty or contains no valid ratings. */ @Nullable public synchronized TvContentRating[] getRatings(String commaSeparatedRatings) { @@ -60,8 +58,8 @@ public final class TvContentRatingCache implements MemoryManageable { if (mRatingsMultiMap.containsKey(commaSeparatedRatings)) { tvContentRatings = mRatingsMultiMap.get(commaSeparatedRatings); } else { - String normalizedRatings = TextUtils - .join(",", getSortedSetFromCsv(commaSeparatedRatings)); + String normalizedRatings = + TextUtils.join(",", getSortedSetFromCsv(commaSeparatedRatings)); if (mRatingsMultiMap.containsKey(normalizedRatings)) { tvContentRatings = mRatingsMultiMap.get(normalizedRatings); } else { @@ -76,9 +74,7 @@ public final class TvContentRatingCache implements MemoryManageable { return tvContentRatings; } - /** - * Returns a sorted array of TvContentRatings from a comma separated string of ratings. - */ + /** Returns a sorted array of TvContentRatings from a comma separated string of ratings. */ @VisibleForTesting static TvContentRating[] stringToContentRatings(String commaSeparatedRatings) { if (TextUtils.isEmpty(commaSeparatedRatings)) { @@ -93,8 +89,9 @@ public final class TvContentRatingCache implements MemoryManageable { Log.e(TAG, "Can't parse the content rating: '" + rating + "'", e); } } - return contentRatings.size() == 0 ? - null : contentRatings.toArray(new TvContentRating[contentRatings.size()]); + return contentRatings.size() == 0 + ? null + : contentRatings.toArray(new TvContentRating[contentRatings.size()]); } private static Set<String> getSortedSetFromCsv(String commaSeparatedRatings) { @@ -118,8 +115,8 @@ public final class TvContentRatingCache implements MemoryManageable { } /** - * Returns a string of each flattened content rating, sorted and concatenated together - * with a comma. + * Returns a string of each flattened content rating, sorted and concatenated together with a + * comma. */ public static String contentRatingsToString(TvContentRating[] contentRatings) { if (contentRatings == null || contentRatings.length == 0) { @@ -141,6 +138,5 @@ public final class TvContentRatingCache implements MemoryManageable { mRatingsMultiMap.clear(); } - private TvContentRatingCache() { - } + private TvContentRatingCache() {} } diff --git a/common/src/com/android/tv/common/WeakHandler.java b/common/src/com/android/tv/common/WeakHandler.java index 188f20ec..dbdd4769 100644 --- a/common/src/com/android/tv/common/WeakHandler.java +++ b/common/src/com/android/tv/common/WeakHandler.java @@ -20,7 +20,6 @@ import android.os.Handler; import android.os.Looper; import android.os.Message; import android.support.annotation.NonNull; - import java.lang.ref.WeakReference; /** @@ -37,7 +36,7 @@ public abstract class WeakHandler<T> extends Handler { private final WeakReference<T> mRef; /** - * Constructs a new handler with a weak reference to the given referent using the provided + * Constructs a new handler with a weak reference to the given referent using the provided * Looper instead of the default one. * * @param looper The looper, must not be null. @@ -57,9 +56,7 @@ public abstract class WeakHandler<T> extends Handler { mRef = new WeakReference<>(ref); } - /** - * Calls {@link #handleMessage(Message, Object)} if the WeakReference is not cleared. - */ + /** Calls {@link #handleMessage(Message, Object)} if the WeakReference is not cleared. */ @Override public final void handleMessage(Message msg) { T referent = mRef.get(); @@ -74,7 +71,7 @@ public abstract class WeakHandler<T> extends Handler { * * <p>If the WeakReference is cleared this method will no longer be called. * - * @param msg the message to handle + * @param msg the message to handle * @param referent the referent. Guaranteed to be non null. */ protected abstract void handleMessage(Message msg, @NonNull T referent); diff --git a/common/src/com/android/tv/common/actions/InputSetupActionUtils.java b/common/src/com/android/tv/common/actions/InputSetupActionUtils.java new file mode 100644 index 00000000..7ba799ed --- /dev/null +++ b/common/src/com/android/tv/common/actions/InputSetupActionUtils.java @@ -0,0 +1,127 @@ +/* + * Copyright 2018 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. + */ + +package com.android.tv.common.actions; + +import android.content.Intent; +import android.media.tv.TvInputInfo; +import android.os.Bundle; +import android.support.annotation.Nullable; + +/** Constants and static utilities for the Input Setup Action. */ +public class InputSetupActionUtils { + + /** + * An intent action to launch setup activity of a TV input. + * + * <p>The intent should include TV input ID in the value of {@link #EXTRA_INPUT_ID}. Optionally, + * given the value of {@link #EXTRA_ACTIVITY_AFTER_COMPLETION}, the activity will be launched + * after the setup activity successfully finishes. + */ + public static final String INTENT_ACTION_INPUT_SETUP = + "com.android.tv.action.LAUNCH_INPUT_SETUP"; + /** + * A constant of the key to indicate a TV input ID for the intent action {@link + * #INTENT_ACTION_INPUT_SETUP}. + * + * <p>Value type: String + */ + public static final String EXTRA_INPUT_ID = TvInputInfo.EXTRA_INPUT_ID; + /** + * A constant of the key for intent to launch actual TV input setup activity used with {@link + * #INTENT_ACTION_INPUT_SETUP}. + * + * <p>Value type: Intent (Parcelable) + */ + public static final String EXTRA_SETUP_INTENT = "com.android.tv.extra.SETUP_INTENT"; + /** + * A constant of the key to indicate an Activity launch intent for the intent action {@link + * #INTENT_ACTION_INPUT_SETUP}. + * + * <p>Value type: Intent (Parcelable) + */ + public static final String EXTRA_ACTIVITY_AFTER_COMPLETION = + "com.android.tv.intent.extra.ACTIVITY_AFTER_COMPLETION"; + /** + * An intent action to launch setup activity of a TV input. + * + * <p>The intent should include TV input ID in the value of {@link #EXTRA_INPUT_ID}. Optionally, + * given the value of {@link #EXTRA_GOOGLE_ACTIVITY_AFTER_COMPLETION}, the activity will be + * launched after the setup activity successfully finishes. + * + * <p>Value type: Intent (Parcelable) + * + * @deprecated Use {@link #INTENT_ACTION_INPUT_SETUP} instead + */ + @Deprecated + private static final String INTENT_GOOGLE_ACTION_INPUT_SETUP = + "com.google.android.tv.action.LAUNCH_INPUT_SETUP"; + /** + * A Google specific constant of the key for intent to launch actual TV input setup activity + * used with {@link #INTENT_ACTION_INPUT_SETUP}. + * + * <p>Value type: Intent (Parcelable) + * + * @deprecated Use {@link #EXTRA_SETUP_INTENT} instead + */ + @Deprecated + private static final String EXTRA_GOOGLE_SETUP_INTENT = + "com.google.android.tv.extra.SETUP_INTENT"; + /** + * A Google specific constant of the key to indicate an Activity launch intent for the intent + * action {@link #INTENT_ACTION_INPUT_SETUP}. + * + * <p>Value type: Intent (Parcelable) + * + * @deprecated Use {@link #EXTRA_ACTIVITY_AFTER_COMPLETION} instead + */ + @Deprecated + private static final String EXTRA_GOOGLE_ACTIVITY_AFTER_COMPLETION = + "com.google.android.tv.intent.extra.ACTIVITY_AFTER_COMPLETION"; + + public static void removeSetupIntent(Bundle extras) { + extras.remove(EXTRA_SETUP_INTENT); + extras.remove(EXTRA_GOOGLE_SETUP_INTENT); + } + + @Nullable + public static Intent getExtraSetupIntent(Intent intent) { + Bundle extras = intent.getExtras(); + if (extras == null) { + return null; + } + Intent setupIntent = extras.getParcelable(EXTRA_SETUP_INTENT); + return setupIntent != null ? setupIntent : extras.getParcelable(EXTRA_GOOGLE_SETUP_INTENT); + } + + @Nullable + public static Intent getExtraActivityAfter(Intent intent) { + Bundle extras = intent.getExtras(); + if (extras == null) { + return null; + } + Intent setupIntent = extras.getParcelable(EXTRA_ACTIVITY_AFTER_COMPLETION); + return setupIntent != null + ? setupIntent + : extras.getParcelable(EXTRA_GOOGLE_ACTIVITY_AFTER_COMPLETION); + } + + public static boolean hasInputSetupAction(Intent intent) { + String action = intent.getAction(); + return INTENT_ACTION_INPUT_SETUP.equals(action) + || INTENT_GOOGLE_ACTION_INPUT_SETUP.equals(action); + } +} diff --git a/common/src/com/android/tv/common/annotation/UsedByNative.java b/common/src/com/android/tv/common/annotation/UsedByNative.java new file mode 100644 index 00000000..dc829e08 --- /dev/null +++ b/common/src/com/android/tv/common/annotation/UsedByNative.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2018 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. + */ + +package com.android.tv.common.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; + +/** + * Annotation used for marking methods and fields that are called from native + * code. Useful for keeping components that would otherwise be removed by + * Proguard. Use the value parameter to mention a file that calls this method. + * + * Note that adding this annotation to a method is not enough to guarantee that + * it is kept - either its class must be referenced elsewhere in the program, or + * the class must be annotated with this as well. + * + * Usage example:<br /> + * <pre>{@code + * @UsedByNative("NativeCrashHandler.cpp") + * public static void reportCrash(int signal, int code, int address) { + * ... + * } + * </pre> + * + */ +@Target({ElementType.METHOD, ElementType.FIELD, ElementType.TYPE, ElementType.CONSTRUCTOR}) +public @interface UsedByNative { + String value(); +} diff --git a/common/src/com/android/tv/common/concurrent/NamedThreadFactory.java b/common/src/com/android/tv/common/concurrent/NamedThreadFactory.java new file mode 100644 index 00000000..4c8371f3 --- /dev/null +++ b/common/src/com/android/tv/common/concurrent/NamedThreadFactory.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2015 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 + */ + +package com.android.tv.common.concurrent; + +import android.support.annotation.NonNull; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicInteger; + +/** A thread factory that creates threads with named <code>prefix-##</code>. */ +public class NamedThreadFactory implements ThreadFactory { + private final AtomicInteger mCount = new AtomicInteger(0); + private final ThreadFactory mDefaultThreadFactory; + private final String mPrefix; + + public NamedThreadFactory(final String prefix) { + mDefaultThreadFactory = Executors.defaultThreadFactory(); + mPrefix = prefix + "-"; + } + + @Override + public Thread newThread(@NonNull final Runnable runnable) { + final Thread thread = mDefaultThreadFactory.newThread(runnable); + thread.setName(mPrefix + mCount.getAndIncrement()); + return thread; + } + + public boolean namedWithPrefix(Thread thread) { + return thread.getName().startsWith(mPrefix); + } +} diff --git a/common/src/com/android/tv/common/config/DefaultConfigManager.java b/common/src/com/android/tv/common/config/DefaultConfigManager.java new file mode 100644 index 00000000..ae240855 --- /dev/null +++ b/common/src/com/android/tv/common/config/DefaultConfigManager.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2016 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. + */ + +package com.android.tv.common.config; + +import android.content.Context; +import com.android.tv.common.config.api.RemoteConfig; + +/** Stub Remote Config. */ +public class DefaultConfigManager { + public static final long DEFAULT_LONG_VALUE = 0; + + public static DefaultConfigManager createInstance(Context context) { + return new DefaultConfigManager(); + } + + private StubRemoteConfig mRemoteConfig = new StubRemoteConfig(); + + public RemoteConfig getRemoteConfig() { + return mRemoteConfig; + } + + private static class StubRemoteConfig implements RemoteConfig { + @Override + public void fetch(OnRemoteConfigUpdatedListener listener) {} + + @Override + public String getString(String key) { + return null; + } + + @Override + public boolean getBoolean(String key) { + return false; + } + + @Override + public long getLong(String key) { + return DEFAULT_LONG_VALUE; + } + + @Override + public long getLong(String key, long defaultValue) { + return defaultValue; + } + } +} diff --git a/common/src/com/android/tv/common/config/RemoteConfigFeature.java b/common/src/com/android/tv/common/config/RemoteConfigFeature.java new file mode 100644 index 00000000..2ea381f0 --- /dev/null +++ b/common/src/com/android/tv/common/config/RemoteConfigFeature.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2016 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 + */ + +package com.android.tv.common.config; + +import android.content.Context; +import com.android.tv.common.BaseApplication; +import com.android.tv.common.feature.Feature; + +/** + * A {@link Feature} controlled by a {@link com.android.tv.common.config.api.RemoteConfig} boolean. + */ +public class RemoteConfigFeature implements Feature { + private final String mKey; + + /** Creates a {@link RemoteConfigFeature for the {@code key}. */ + public static RemoteConfigFeature fromKey(String key) { + return new RemoteConfigFeature(key); + } + + private RemoteConfigFeature(String key) { + mKey = key; + } + + @Override + public boolean isEnabled(Context context) { + return BaseApplication.getSingletons(context).getRemoteConfig().getBoolean(mKey); + } +} diff --git a/common/src/com/android/tv/common/config/api/RemoteConfig.java b/common/src/com/android/tv/common/config/api/RemoteConfig.java new file mode 100644 index 00000000..74597f9d --- /dev/null +++ b/common/src/com/android/tv/common/config/api/RemoteConfig.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2016 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. + */ + +package com.android.tv.common.config.api; + +/** + * Manages Live TV Configuration, allowing remote updates. + * + * <p>This is a thin wrapper around <a + * href="https://firebase.google.com/docs/remote-config/"></a>Firebase Remote Config</a> + */ +public interface RemoteConfig { + + /** Used to inject a remote config */ + interface HasRemoteConfig { + RemoteConfig getRemoteConfig(); + } + + /** Notified on successful completion of a {@link #fetch)} */ + interface OnRemoteConfigUpdatedListener { + void onRemoteConfigUpdated(); + } + + /** Starts a fetch and notifies {@code listener} on successful completion. */ + void fetch(OnRemoteConfigUpdatedListener listener); + + /** Gets value as a string corresponding to the specified key. */ + String getString(String key); + + /** Gets value as a boolean corresponding to the specified key. */ + boolean getBoolean(String key); + + /** Gets value as a long corresponding to the specified key. */ + long getLong(String key); + + /** + * Gets value as a long corresponding to the specified key. Returns the defaultValue if no value + * is found. + */ + long getLong(String key, long defaultValue); +} diff --git a/common/src/com/android/tv/common/config/api/RemoteConfigValue.java b/common/src/com/android/tv/common/config/api/RemoteConfigValue.java new file mode 100644 index 00000000..6da89fb9 --- /dev/null +++ b/common/src/com/android/tv/common/config/api/RemoteConfigValue.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2018 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 + */ +package com.android.tv.common.config.api; + +/** Wrapper for a RemoteConfig key and default value. */ +public abstract class RemoteConfigValue<T> { + private final T defaultValue; + private final String key; + + private RemoteConfigValue(String key, T defaultValue) { + this.defaultValue = defaultValue; + this.key = key; + } + + /** Create with the given key and default value */ + public static RemoteConfigValue<Long> create(String key, long defaultValue) { + return new RemoteConfigValue<Long>(key, defaultValue) { + @Override + public Long get(RemoteConfig remoteConfig) { + return remoteConfig.getLong(key, defaultValue); + } + }; + } + + public abstract T get(RemoteConfig remoteConfig); + + public final T getDefaultValue() { + return defaultValue; + } + + @Override + public final String toString() { + return "RemoteConfigValue(key=" + key + ", defalutValue=" + defaultValue + "]"; + } +} diff --git a/common/src/com/android/tv/common/customization/CustomAction.java b/common/src/com/android/tv/common/customization/CustomAction.java new file mode 100644 index 00000000..ee18cb3c --- /dev/null +++ b/common/src/com/android/tv/common/customization/CustomAction.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2015 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. + */ + +package com.android.tv.common.customization; + +import android.content.Intent; +import android.graphics.drawable.Drawable; +import android.support.annotation.NonNull; + +/** Describes a custom option defined in customization package. This will be added to main menu. */ +public class CustomAction implements Comparable<CustomAction> { + private static final int POSITION_THRESHOLD = 100; + + private final int mPositionPriority; + private final String mTitle; + private final Drawable mIconDrawable; + private final Intent mIntent; + + public CustomAction(int positionPriority, String title, Drawable iconDrawable, Intent intent) { + mPositionPriority = positionPriority; + mTitle = title; + mIconDrawable = iconDrawable; + mIntent = intent; + } + + /** + * Returns if this option comes before the existing items. Note that custom options can only be + * placed at the front or back. (i.e. cannot be added in the middle of existing options.) + * + * @return {@code true} if it goes to the beginning. {@code false} if it goes to the end. + */ + public boolean isFront() { + return mPositionPriority < POSITION_THRESHOLD; + } + + @Override + public int compareTo(@NonNull CustomAction another) { + return mPositionPriority - another.mPositionPriority; + } + + /** Returns title. */ + public String getTitle() { + return mTitle; + } + + /** Returns icon drawable. */ + public Drawable getIconDrawable() { + return mIconDrawable; + } + + /** Returns intent to launch when this option is clicked. */ + public Intent getIntent() { + return mIntent; + } +} diff --git a/common/src/com/android/tv/common/customization/CustomizationManager.java b/common/src/com/android/tv/common/customization/CustomizationManager.java new file mode 100644 index 00000000..09ecaef8 --- /dev/null +++ b/common/src/com/android/tv/common/customization/CustomizationManager.java @@ -0,0 +1,273 @@ +/* + * Copyright (C) 2015 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. + */ + +package com.android.tv.common.customization; + +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.ResolveInfo; +import android.content.res.Resources; +import android.graphics.drawable.Drawable; +import android.support.annotation.IntDef; +import android.text.TextUtils; +import android.util.Log; +import com.android.tv.common.CommonConstants; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class CustomizationManager { + private static final String TAG = "CustomizationManager"; + private static final boolean DEBUG = false; + + private static final String[] CUSTOMIZE_PERMISSIONS = { + CommonConstants.BASE_PACKAGE + ".permission.CUSTOMIZE_TV_APP" + }; + + private static final String CATEGORY_TV_CUSTOMIZATION = + CommonConstants.BASE_PACKAGE + ".category"; + + /** Row IDs to share customized actions. Only rows listed below can have customized action. */ + public static final String ID_OPTIONS_ROW = "options_row"; + + public static final String ID_PARTNER_ROW = "partner_row"; + + @IntDef({TRICKPLAY_MODE_ENABLED, TRICKPLAY_MODE_DISABLED, TRICKPLAY_MODE_USE_EXTERNAL_STORAGE}) + @Retention(RetentionPolicy.SOURCE) + public @interface TRICKPLAY_MODE {} + + public static final int TRICKPLAY_MODE_ENABLED = 0; + public static final int TRICKPLAY_MODE_DISABLED = 1; + public static final int TRICKPLAY_MODE_USE_EXTERNAL_STORAGE = 2; + + private static final String[] TRICKPLAY_MODE_STRINGS = { + "enabled", "disabled", "use_external_storage_only" + }; + + private static final HashMap<String, String> INTENT_CATEGORY_TO_ROW_ID; + + static { + INTENT_CATEGORY_TO_ROW_ID = new HashMap<>(); + INTENT_CATEGORY_TO_ROW_ID.put(CATEGORY_TV_CUSTOMIZATION + ".OPTIONS_ROW", ID_OPTIONS_ROW); + INTENT_CATEGORY_TO_ROW_ID.put(CATEGORY_TV_CUSTOMIZATION + ".PARTNER_ROW", ID_PARTNER_ROW); + } + + private static final String RES_ID_PARTNER_ROW_TITLE = "partner_row_title"; + private static final String RES_ID_HAS_LINUX_DVB_BUILT_IN_TUNER = + "has_linux_dvb_built_in_tuner"; + private static final String RES_ID_TRICKPLAY_MODE = "trickplay_mode"; + + private static final String RES_TYPE_STRING = "string"; + private static final String RES_TYPE_BOOLEAN = "bool"; + + private static String sCustomizationPackage; + private static Boolean sHasLinuxDvbBuiltInTuner; + private static @TRICKPLAY_MODE Integer sTrickplayMode; + + private final Context mContext; + private boolean mInitialized; + + private String mPartnerRowTitle; + private final Map<String, List<CustomAction>> mRowIdToCustomActionsMap = new HashMap<>(); + + public CustomizationManager(Context context) { + mContext = context; + mInitialized = false; + } + + /** + * Returns {@code true} if there's a customization package installed and it specifies built-in + * tuner devices are available. The built-in tuner should support DVB API to be recognized by + * Live TV. + */ + public static boolean hasLinuxDvbBuiltInTuner(Context context) { + if (sHasLinuxDvbBuiltInTuner == null) { + if (TextUtils.isEmpty(getCustomizationPackageName(context))) { + sHasLinuxDvbBuiltInTuner = false; + } else { + try { + Resources res = + context.getPackageManager() + .getResourcesForApplication(sCustomizationPackage); + int resId = + res.getIdentifier( + RES_ID_HAS_LINUX_DVB_BUILT_IN_TUNER, + RES_TYPE_BOOLEAN, + sCustomizationPackage); + sHasLinuxDvbBuiltInTuner = resId != 0 && res.getBoolean(resId); + } catch (NameNotFoundException e) { + sHasLinuxDvbBuiltInTuner = false; + } + } + } + return sHasLinuxDvbBuiltInTuner; + } + + public static @TRICKPLAY_MODE int getTrickplayMode(Context context) { + if (sTrickplayMode == null) { + if (TextUtils.isEmpty(getCustomizationPackageName(context))) { + sTrickplayMode = TRICKPLAY_MODE_ENABLED; + } else { + try { + String customization = null; + Resources res = + context.getPackageManager() + .getResourcesForApplication(sCustomizationPackage); + int resId = + res.getIdentifier( + RES_ID_TRICKPLAY_MODE, RES_TYPE_STRING, sCustomizationPackage); + customization = resId == 0 ? null : res.getString(resId); + sTrickplayMode = TRICKPLAY_MODE_ENABLED; + if (customization != null) { + for (int i = 0; i < TRICKPLAY_MODE_STRINGS.length; ++i) { + if (TRICKPLAY_MODE_STRINGS[i].equalsIgnoreCase(customization)) { + sTrickplayMode = i; + break; + } + } + } + } catch (NameNotFoundException e) { + sTrickplayMode = TRICKPLAY_MODE_ENABLED; + } + } + } + return sTrickplayMode; + } + + private static String getCustomizationPackageName(Context context) { + if (sCustomizationPackage == null) { + List<PackageInfo> packageInfos = + context.getPackageManager() + .getPackagesHoldingPermissions(CUSTOMIZE_PERMISSIONS, 0); + sCustomizationPackage = packageInfos.size() == 0 ? "" : packageInfos.get(0).packageName; + } + return sCustomizationPackage; + } + + /** Initialize TV customization options. Run this API only on the main thread. */ + public void initialize() { + if (mInitialized) { + return; + } + mInitialized = true; + if (!TextUtils.isEmpty(getCustomizationPackageName(mContext))) { + buildCustomActions(); + buildPartnerRow(); + } + } + + private void buildCustomActions() { + mRowIdToCustomActionsMap.clear(); + PackageManager pm = mContext.getPackageManager(); + for (String intentCategory : INTENT_CATEGORY_TO_ROW_ID.keySet()) { + Intent customOptionIntent = new Intent(Intent.ACTION_MAIN); + customOptionIntent.addCategory(intentCategory); + + List<ResolveInfo> activities = + pm.queryIntentActivities( + customOptionIntent, + PackageManager.GET_RECEIVERS + | PackageManager.GET_RESOLVED_FILTER + | PackageManager.GET_META_DATA); + for (ResolveInfo info : activities) { + String packageName = info.activityInfo.packageName; + if (!TextUtils.equals(packageName, sCustomizationPackage)) { + Log.w( + TAG, + "A customization package " + + sCustomizationPackage + + " already exist. Ignoring " + + packageName); + continue; + } + + int position = info.filter.getPriority(); + String title = info.loadLabel(pm).toString(); + Drawable drawable = info.loadIcon(pm); + Intent intent = new Intent(Intent.ACTION_MAIN); + intent.addCategory(intentCategory); + intent.setClassName(sCustomizationPackage, info.activityInfo.name); + + String rowId = INTENT_CATEGORY_TO_ROW_ID.get(intentCategory); + List<CustomAction> actions = mRowIdToCustomActionsMap.get(rowId); + if (actions == null) { + actions = new ArrayList<>(); + mRowIdToCustomActionsMap.put(rowId, actions); + } + actions.add(new CustomAction(position, title, drawable, intent)); + } + } + // Sort items by position + for (List<CustomAction> actions : mRowIdToCustomActionsMap.values()) { + Collections.sort(actions); + } + + if (DEBUG) { + Log.d(TAG, "Dumping custom actions"); + for (String id : mRowIdToCustomActionsMap.keySet()) { + for (CustomAction action : mRowIdToCustomActionsMap.get(id)) { + Log.d( + TAG, + "Custom row rowId=" + + id + + " title=" + + action.getTitle() + + " class=" + + action.getIntent()); + } + } + Log.d(TAG, "Dumping custom actions - end of dump"); + } + } + + /** + * Returns custom actions for given row id. + * + * <p>Row ID is one of ID_OPTIONS_ROW or ID_PARTNER_ROW. + */ + public List<CustomAction> getCustomActions(String rowId) { + return mRowIdToCustomActionsMap.get(rowId); + } + + private void buildPartnerRow() { + mPartnerRowTitle = null; + Resources res; + try { + res = mContext.getPackageManager().getResourcesForApplication(sCustomizationPackage); + } catch (NameNotFoundException e) { + Log.w(TAG, "Could not get resources for package " + sCustomizationPackage); + return; + } + int resId = + res.getIdentifier(RES_ID_PARTNER_ROW_TITLE, RES_TYPE_STRING, sCustomizationPackage); + if (resId != 0) { + mPartnerRowTitle = res.getString(resId); + } + if (DEBUG) Log.d(TAG, "Partner row title [" + mPartnerRowTitle + "]"); + } + + /** Returns partner row title. */ + public String getPartnerRowTitle() { + return mPartnerRowTitle; + } +} diff --git a/common/src/com/android/tv/common/experiments/ExperimentFlag.java b/common/src/com/android/tv/common/experiments/ExperimentFlag.java new file mode 100644 index 00000000..c9bacac4 --- /dev/null +++ b/common/src/com/android/tv/common/experiments/ExperimentFlag.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2016 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. + */ + +package com.android.tv.common.experiments; + +import android.support.annotation.VisibleForTesting; +import com.android.tv.common.BuildConfig; + + +/** Experiments return values based on user, device and other criteria. */ +public final class ExperimentFlag<T> { + + private static boolean sAllowOverrides = false; + + @VisibleForTesting + public static void initForTest() { + sAllowOverrides = true; + } + + /** Returns a boolean experiment */ + public static ExperimentFlag<Boolean> createFlag( + boolean defaultValue) { + return new ExperimentFlag<>( + defaultValue); + } + + private final T mDefaultValue; + + private T mOverrideValue = null; + private boolean mOverridden = false; + + private ExperimentFlag( + T defaultValue) { + mDefaultValue = defaultValue; + } + + /** Returns value for this experiment */ + public T get() { + return sAllowOverrides && mOverridden ? mOverrideValue : mDefaultValue; + } + + @VisibleForTesting + public void override(T t) { + if (sAllowOverrides) { + mOverridden = true; + mOverrideValue = t; + } + } + + @VisibleForTesting + public void resetOverride() { + mOverridden = false; + } +} diff --git a/common/src/com/android/tv/common/annotation/UsedByReflection.java b/common/src/com/android/tv/common/experiments/ExperimentLoader.java index 5a4517f7..5f012e11 100644 --- a/common/src/com/android/tv/common/annotation/UsedByReflection.java +++ b/common/src/com/android/tv/common/experiments/ExperimentLoader.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2015 The Android Open Source Project + * Copyright (C) 2018 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. @@ -14,16 +14,15 @@ * limitations under the License. */ -package com.android.tv.common.annotation; +package com.android.tv.common.experiments; -import static java.lang.annotation.RetentionPolicy.SOURCE; +import android.content.Context; -import java.lang.annotation.Retention; +/** Used to sync {@link ExperimentFlag}s. */ +public class ExperimentLoader { -/** - * Denotes that the method or field is used by reflection even though it is not ever called - * directly. - */ -@Retention(SOURCE) -public @interface UsedByReflection { + /** Starts a background task to update {@link ExperimentFlag}s */ + public void asyncRefreshExperiments(Context context) { + // Override for your experiment system + } } diff --git a/common/src/com/android/tv/common/experiments/Experiments.java b/common/src/com/android/tv/common/experiments/Experiments.java new file mode 100644 index 00000000..96b15e53 --- /dev/null +++ b/common/src/com/android/tv/common/experiments/Experiments.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2016 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. + */ + +package com.android.tv.common.experiments; + +import static com.android.tv.common.experiments.ExperimentFlag.createFlag; + +import com.android.tv.common.BuildConfig; + +/** + * Set of experiments visible in AOSP. + * + * <p>This file is maintained by hand. + */ +public final class Experiments { + public static final ExperimentFlag<Boolean> CLOUD_EPG = + ExperimentFlag.createFlag( + true); + + public static final ExperimentFlag<Boolean> ENABLE_UNRATED_CONTENT_SETTINGS = + ExperimentFlag.createFlag( + false); + + /** Turn analytics on or off based on the System Checkbox for logging. */ + public static final ExperimentFlag<Boolean> ENABLE_ANALYTICS_VIA_CHECKBOX = + createFlag( + false); + + /** + * Allow developer features such as the dev menu and other aids. + * + * <p>These features are available to select users(aka fishfooders) on production builds. + */ + public static final ExperimentFlag<Boolean> ENABLE_DEVELOPER_FEATURES = + ExperimentFlag.createFlag( + BuildConfig.ENG); + + /** + * Allow QA features. + * + * <p>These features must be carefully limited, keeping QA differences to a minimum. + * + * <p>These features are available to select users(aka QA) on production builds. + */ + public static final ExperimentFlag<Boolean> ENABLE_QA_FEATURES = + ExperimentFlag.createFlag( + false); + + private Experiments() {} +} diff --git a/common/src/com/android/tv/common/feature/CommonFeatures.java b/common/src/com/android/tv/common/feature/CommonFeatures.java index 62c88ead..1fceabb3 100644 --- a/common/src/com/android/tv/common/feature/CommonFeatures.java +++ b/common/src/com/android/tv/common/feature/CommonFeatures.java @@ -19,36 +19,81 @@ package com.android.tv.common.feature; import static com.android.tv.common.feature.FeatureUtils.AND; import static com.android.tv.common.feature.TestableFeature.createTestableFeature; +import android.content.Context; +import android.os.Build; +import android.text.TextUtils; +import android.util.Log; +import com.android.tv.common.config.api.RemoteConfig.HasRemoteConfig; +import com.android.tv.common.experiments.Experiments; + +import com.android.tv.common.util.CommonUtils; +import com.android.tv.common.util.LocationUtils; + /** * List of {@link Feature} that affect more than just the Live TV app. * * <p>Remove the {@code Feature} once it is launched. */ public class CommonFeatures { + private static final String TAG = "CommonFeatures"; + private static final boolean DEBUG = false; + /** * DVR * * <p>See <a href="https://goto.google.com/atv-dvr-onepager">go/atv-dvr-onepager</a> * - * DVR API is introduced in N, it only works when app runs as a system app. + * <p>DVR API is introduced in N, it only works when app runs as a system app. */ - public static final TestableFeature DVR = createTestableFeature( - AND(Sdk.AT_LEAST_N, SystemAppFeature.SYSTEM_APP_FEATURE)); + public static final TestableFeature DVR = + createTestableFeature(AND(Sdk.AT_LEAST_N, SystemAppFeature.SYSTEM_APP_FEATURE)); /** * ENABLE_RECORDING_REGARDLESS_OF_STORAGE_STATUS * - * Enables dvr recording regardless of storage status. + * <p>Enables dvr recording regardless of storage status. */ public static final Feature FORCE_RECORDING_UNTIL_NO_SPACE = - new PropertyFeature("force_recording_until_no_space", false); + PropertyFeature.create("force_recording_until_no_space", false); - /** - * USE_SW_CODEC_FOR_SD - * - * Prefer software based codec for SD channels. - */ - public static final Feature USE_SW_CODEC_FOR_SD = - new PropertyFeature("use_sw_codec_for_sd", false - ); + public static final Feature TUNER = + new Feature() { + @Override + public boolean isEnabled(Context context) { + + if (CommonUtils.isDeveloper()) { + // we enable tuner for developers to test tuner in any platform. + return true; + } + + // This is special handling just for USB Tuner. + // It does not require any N API's but relies on a improvements in N for AC3 + // support + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N; + } + }; + + /** Show postal code fragment before channel scan. */ + public static final Feature ENABLE_CLOUD_EPG_REGION = + new Feature() { + private final String[] supportedRegions = { + }; + + + @Override + public boolean isEnabled(Context context) { + if (!Experiments.CLOUD_EPG.get()) { + if (DEBUG) Log.d(TAG, "Experiments.CLOUD_EPG is false"); + return false; + } + String country = LocationUtils.getCurrentCountry(context); + for (int i = 0; i < supportedRegions.length; i++) { + if (supportedRegions[i].equalsIgnoreCase(country)) { + return true; + } + } + if (DEBUG) Log.d(TAG, "EPG flag false after country check"); + return false; + } + }; } diff --git a/common/src/com/android/tv/common/feature/EngOnlyFeature.java b/common/src/com/android/tv/common/feature/EngOnlyFeature.java index 9fc39d9f..5feb5481 100644 --- a/common/src/com/android/tv/common/feature/EngOnlyFeature.java +++ b/common/src/com/android/tv/common/feature/EngOnlyFeature.java @@ -17,16 +17,13 @@ package com.android.tv.common.feature; import android.content.Context; - import com.android.tv.common.BuildConfig; -/** - * A feature that is only available on {@link BuildConfig#ENG} builds. - */ +/** A feature that is only available on {@link BuildConfig#ENG} builds. */ public final class EngOnlyFeature implements Feature { public static final Feature ENG_ONLY_FEATURE = new EngOnlyFeature(); - private EngOnlyFeature() { } + private EngOnlyFeature() {} @Override public boolean isEnabled(Context context) { diff --git a/common/src/com/android/tv/common/feature/ExperimentFeature.java b/common/src/com/android/tv/common/feature/ExperimentFeature.java new file mode 100644 index 00000000..820eda49 --- /dev/null +++ b/common/src/com/android/tv/common/feature/ExperimentFeature.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2017 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 + */ + +package com.android.tv.common.feature; + +import android.content.Context; +import com.android.tv.common.experiments.ExperimentFlag; + +/** A {@link Feature} base on an {@link ExperimentFlag}. */ +public final class ExperimentFeature implements Feature { + + public static Feature from(ExperimentFlag<Boolean> flag) { + return new ExperimentFeature(flag); + } + + private final ExperimentFlag<Boolean> mFlag; + + private ExperimentFeature(ExperimentFlag<Boolean> flag) { + mFlag = flag; + } + + @Override + public boolean isEnabled(Context context) { + return mFlag.get(); + } + + @Override + public String toString() { + return "ExperimentFeature for " + mFlag; + } +} diff --git a/common/src/com/android/tv/common/feature/Feature.java b/common/src/com/android/tv/common/feature/Feature.java index b3a8336c..fd5625e5 100644 --- a/common/src/com/android/tv/common/feature/Feature.java +++ b/common/src/com/android/tv/common/feature/Feature.java @@ -23,16 +23,15 @@ import android.content.Context; * launched. * * <p>Expected usage is: + * * <pre>{@code - * if (MY_FEATURE.isEnabled(context) { - * showNewCoolUi(); - * } else{ - * showOldBoringUi(); - * } + * if (MY_FEATURE.isEnabled(context) { + * showNewCoolUi(); + * } else{ + * showOldBoringUi(); + * } * }</pre> */ public interface Feature { boolean isEnabled(Context context); - - } diff --git a/common/src/com/android/tv/common/feature/FeatureUtils.java b/common/src/com/android/tv/common/feature/FeatureUtils.java index f60b2048..8650d151 100644 --- a/common/src/com/android/tv/common/feature/FeatureUtils.java +++ b/common/src/com/android/tv/common/feature/FeatureUtils.java @@ -17,12 +17,10 @@ package com.android.tv.common.feature; import android.content.Context; - +import com.android.tv.common.util.CommonUtils; import java.util.Arrays; -/** - * Static utilities for features. - */ +/** Static utilities for features. */ public class FeatureUtils { /** @@ -47,7 +45,6 @@ public class FeatureUtils { return "or(" + Arrays.asList(features) + ")"; } }; - } /** @@ -74,36 +71,47 @@ public class FeatureUtils { }; } - /** - * A feature that is always enabled. - */ - public static final Feature ON = new Feature() { - @Override - public boolean isEnabled(Context context) { - return true; - } + /** A feature that is always enabled. */ + public static final Feature ON = + new Feature() { + @Override + public boolean isEnabled(Context context) { + return true; + } - @Override - public String toString() { - return "on"; - } - }; + @Override + public String toString() { + return "on"; + } + }; - /** - * A feature that is always disabled. - */ - public static final Feature OFF = new Feature() { - @Override - public boolean isEnabled(Context context) { - return false; - } + /** A feature that is always disabled. */ + public static final Feature OFF = + new Feature() { + @Override + public boolean isEnabled(Context context) { + return false; + } - @Override - public String toString() { - return "off"; - } - }; + @Override + public String toString() { + return "off"; + } + }; - private FeatureUtils() { - } + /** True if running in robolectric. */ + public static final Feature ROBOLECTRIC = + new Feature() { + @Override + public boolean isEnabled(Context context) { + return CommonUtils.isRoboTest(); + } + + @Override + public String toString() { + return "isRobolecteric"; + } + }; + + private FeatureUtils() {} } diff --git a/common/src/com/android/tv/common/feature/GServiceFeature.java b/common/src/com/android/tv/common/feature/GServiceFeature.java index 9e6e11a6..1d7d1156 100644 --- a/common/src/com/android/tv/common/feature/GServiceFeature.java +++ b/common/src/com/android/tv/common/feature/GServiceFeature.java @@ -18,10 +18,7 @@ package com.android.tv.common.feature; import android.content.Context; - -/** - * A feature controlled by a GServices flag. - */ +/** A feature controlled by a GServices flag. */ public class GServiceFeature implements Feature { private static final String LIVECHANNELS_PREFIX = "livechannels:"; private final String mKey; diff --git a/common/src/com/android/tv/common/feature/Model.java b/common/src/com/android/tv/common/feature/Model.java new file mode 100644 index 00000000..7aa5148e --- /dev/null +++ b/common/src/com/android/tv/common/feature/Model.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2018 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 + */ + +package com.android.tv.common.feature; + +import android.content.Context; + +/** Holder for {@link android.os.Build#MODEL} features. */ +public interface Model { + + ModelFeature NEXUS_PLAYER = new ModelFeature("Nexus Player"); + + /** True when the {@link android.os.Build#MODEL} equals the {@code model} given. */ + public static final class ModelFeature implements Feature { + private final String mModel; + + private ModelFeature(String model) { + mModel = model; + } + + @Override + public boolean isEnabled(Context context) { + return isEnabled(); + } + + public boolean isEnabled() { + return android.os.Build.MODEL.equals(mModel); + } + + @Override + public String toString() { + return "ModelFeature(" + mModel + ")=" + isEnabled(); + } + } +} diff --git a/common/src/com/android/tv/common/feature/PackageVersionFeature.java b/common/src/com/android/tv/common/feature/PackageVersionFeature.java index 7f615d31..f41a14a0 100644 --- a/common/src/com/android/tv/common/feature/PackageVersionFeature.java +++ b/common/src/com/android/tv/common/feature/PackageVersionFeature.java @@ -49,7 +49,10 @@ public class PackageVersionFeature implements Feature { @Override public String toString() { - return "PackageVersionFeature[packageName=" + mPackageName + ",requiredVersion=" - + mRequiredVersionCode + "]"; + return "PackageVersionFeature[packageName=" + + mPackageName + + ",requiredVersion=" + + mRequiredVersionCode + + "]"; } } diff --git a/common/src/com/android/tv/common/feature/PropertyFeature.java b/common/src/com/android/tv/common/feature/PropertyFeature.java index fdcffa04..0cf36317 100644 --- a/common/src/com/android/tv/common/feature/PropertyFeature.java +++ b/common/src/com/android/tv/common/feature/PropertyFeature.java @@ -17,7 +17,6 @@ package com.android.tv.common.feature; import android.content.Context; - import com.android.tv.common.BooleanSystemProperty; /** @@ -26,21 +25,29 @@ import com.android.tv.common.BooleanSystemProperty; * <p>See {@link BooleanSystemProperty} for instructions on how to set using adb. */ public final class PropertyFeature implements Feature { + + public static PropertyFeature create(String key, boolean defaultValue) { + return new PropertyFeature(key, defaultValue); + } + private final BooleanSystemProperty mProperty; /** * Create System Property backed feature. * - * @param key the system property key. Length must be <= 31 characters. + * @param key the system property key. Length must be <= 31 characters. * @param defaultValue the value to return if the property is undefined or empty. */ - public PropertyFeature(String key, boolean defaultValue) { + private PropertyFeature(String key, boolean defaultValue) { if (key.length() > 31) { // Since Features are initialized at startup and the keys are static go ahead and kill // the application. throw new IllegalArgumentException( - "Property keys have a max length of 31 characters but '" + key + "' is " + key - .length() + " characters."); + "Property keys have a max length of 31 characters but '" + + key + + "' is " + + key.length() + + " characters."); } mProperty = new BooleanSystemProperty(key, defaultValue); } diff --git a/common/src/com/android/tv/common/feature/Sdk.java b/common/src/com/android/tv/common/feature/Sdk.java index 9f99a64f..155b391d 100644 --- a/common/src/com/android/tv/common/feature/Sdk.java +++ b/common/src/com/android/tv/common/feature/Sdk.java @@ -19,10 +19,8 @@ package com.android.tv.common.feature; import android.content.Context; import android.os.Build; -/** - * Holder for SDK version features - */ -public class Sdk { +/** Holder for SDK version features */ +public final class Sdk { public static final Feature AT_LEAST_N = new Feature() { @Override @@ -31,5 +29,13 @@ public class Sdk { } }; + public static final Feature AT_LEAST_O = + new Feature() { + @Override + public boolean isEnabled(Context context) { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O; + } + }; + private Sdk() {} } diff --git a/common/src/com/android/tv/common/feature/SharedPreferencesFeature.java b/common/src/com/android/tv/common/feature/SharedPreferencesFeature.java index 881f53d6..ef2260d6 100644 --- a/common/src/com/android/tv/common/feature/SharedPreferencesFeature.java +++ b/common/src/com/android/tv/common/feature/SharedPreferencesFeature.java @@ -19,11 +19,9 @@ package com.android.tv.common.feature; import android.content.Context; import android.content.SharedPreferences; import android.util.Log; -import com.android.tv.common.SharedPreferencesUtils; +import com.android.tv.common.util.SharedPreferencesUtils; -/** - * Feature controlled by shared preferences. - */ +/** Feature controlled by shared preferences. */ public final class SharedPreferencesFeature implements Feature { private static final String TAG = "SharedPrefFeature"; private static final boolean DEBUG = false; @@ -53,8 +51,9 @@ public final class SharedPreferencesFeature implements Feature { return false; } if (mSharedPreferences == null) { - mSharedPreferences = context.getSharedPreferences( - SharedPreferencesUtils.SHARED_PREF_FEATURES, Context.MODE_PRIVATE); + mSharedPreferences = + context.getSharedPreferences( + SharedPreferencesUtils.SHARED_PREF_FEATURES, Context.MODE_PRIVATE); mEnabled = mSharedPreferences.getBoolean(mKey, mDefaultValue); } if (DEBUG) Log.d(TAG, mKey + " is " + mEnabled); @@ -69,14 +68,14 @@ public final class SharedPreferencesFeature implements Feature { public void setEnabled(Context context, boolean enable) { if (DEBUG) Log.d(TAG, mKey + " is set to " + enable); if (mSharedPreferences == null) { - mSharedPreferences = context.getSharedPreferences( - SharedPreferencesUtils.SHARED_PREF_FEATURES, Context.MODE_PRIVATE); + mSharedPreferences = + context.getSharedPreferences( + SharedPreferencesUtils.SHARED_PREF_FEATURES, Context.MODE_PRIVATE); mEnabled = enable; mSharedPreferences.edit().putBoolean(mKey, enable).apply(); } else if (mEnabled != enable) { mEnabled = enable; mSharedPreferences.edit().putBoolean(mKey, enable).apply(); } - } } diff --git a/common/src/com/android/tv/common/feature/SystemAppFeature.java b/common/src/com/android/tv/common/feature/SystemAppFeature.java index 79fd32f3..1dd3b8dd 100644 --- a/common/src/com/android/tv/common/feature/SystemAppFeature.java +++ b/common/src/com/android/tv/common/feature/SystemAppFeature.java @@ -19,13 +19,11 @@ package com.android.tv.common.feature; import android.content.Context; import android.content.pm.ApplicationInfo; -/** - * A feature that is for system App. - */ +/** A feature that is for system App. */ public final class SystemAppFeature implements Feature { public static final Feature SYSTEM_APP_FEATURE = new SystemAppFeature(); - private SystemAppFeature() { } + private SystemAppFeature() {} @Override public boolean isEnabled(Context context) { diff --git a/common/src/com/android/tv/common/feature/TestableFeature.java b/common/src/com/android/tv/common/feature/TestableFeature.java index d7e707a1..1f18639d 100644 --- a/common/src/com/android/tv/common/feature/TestableFeature.java +++ b/common/src/com/android/tv/common/feature/TestableFeature.java @@ -19,33 +19,28 @@ package com.android.tv.common.feature; import android.content.Context; import android.support.annotation.VisibleForTesting; import android.util.Log; - -import com.android.tv.common.TvCommonUtils; +import com.android.tv.common.util.CommonUtils; /** * When run in a test harness this feature can be turned on or off, overriding the normal value. * - * <p><b>Warning</b> making a feature testable will cause the code to stay in the APK and - * could leak unreleased features. + * <p><b>Warning</b> making a feature testable will cause the code to stay in the APK and could leak + * unreleased features. */ public class TestableFeature implements Feature { - private final static String TAG = "TestableFeature"; - private final static String DETAIL_MESSAGE - = "TestableFeatures should only be changed in tests."; + private static final String TAG = "TestableFeature"; + private static final String DETAIL_MESSAGE = + "TestableFeatures should only be changed in tests."; private final Feature mDelegate; private Boolean mTestValue = null; - /** - * Creates testable feature. - */ + /** Creates testable feature. */ public static TestableFeature createTestableFeature(Feature delegate) { return new TestableFeature(delegate); } - /** - * Creates testable feature with initial value. - */ + /** Creates testable feature with initial value. */ public static TestableFeature createTestableFeature(Feature delegate, Boolean initialValue) { return new TestableFeature(delegate, initialValue); } @@ -61,9 +56,8 @@ public class TestableFeature implements Feature { @VisibleForTesting public void enableForTest() { - if (!TvCommonUtils.isRunningInTest()) { - Log.e(TAG, "Not enabling for test:" + this, - new IllegalStateException(DETAIL_MESSAGE)); + if (!CommonUtils.isRunningInTest()) { + Log.e(TAG, "Not enabling for test:" + this, new IllegalStateException(DETAIL_MESSAGE)); } else { mTestValue = true; } @@ -71,8 +65,10 @@ public class TestableFeature implements Feature { @VisibleForTesting public void disableForTests() { - if (!TvCommonUtils.isRunningInTest()) { - Log.e(TAG, "Not disabling for test: " + this, + if (!CommonUtils.isRunningInTest()) { + Log.e( + TAG, + "Not disabling for test: " + this, new IllegalStateException(DETAIL_MESSAGE)); } else { mTestValue = false; @@ -81,7 +77,7 @@ public class TestableFeature implements Feature { @VisibleForTesting public void resetForTests() { - if (!TvCommonUtils.isRunningInTest()) { + if (!CommonUtils.isRunningInTest()) { Log.e(TAG, "Not resetting feature: " + this, new IllegalStateException(DETAIL_MESSAGE)); } else { mTestValue = null; @@ -90,7 +86,7 @@ public class TestableFeature implements Feature { @Override public boolean isEnabled(Context context) { - if (TvCommonUtils.isRunningInTest() && mTestValue != null) { + if (CommonUtils.isRunningInTest() && mTestValue != null) { return mTestValue; } return mDelegate.isEnabled(context); @@ -99,7 +95,7 @@ public class TestableFeature implements Feature { @Override public String toString() { String msg = mDelegate.toString(); - if (TvCommonUtils.isRunningInTest()) { + if (CommonUtils.isRunningInTest()) { if (mTestValue == null) { msg = "Testable Feature is unchanged: " + msg; } else { diff --git a/common/src/com/android/tv/common/MemoryManageable.java b/common/src/com/android/tv/common/memory/MemoryManageable.java index 0cb36103..3e81fb5e 100644 --- a/common/src/com/android/tv/common/MemoryManageable.java +++ b/common/src/com/android/tv/common/memory/MemoryManageable.java @@ -14,16 +14,13 @@ * limitations under the License */ -package com.android.tv.common; +package com.android.tv.common.memory; /** - * Interface for the fine-grained memory management. - * The class which wants to release memory based on the system constraints should inherit - * this interface and implement {@link #performTrimMemory}. + * Interface for the fine-grained memory management. The class which wants to release memory based + * on the system constraints should inherit this interface and implement {@link #performTrimMemory}. */ public interface MemoryManageable { - /** - * For more information, see {@link android.content.ComponentCallbacks2#onTrimMemory}. - */ + /** For more information, see {@link android.content.ComponentCallbacks2#onTrimMemory}. */ void performTrimMemory(int level); } diff --git a/common/src/com/android/tv/common/recording/RecordingCapability.java b/common/src/com/android/tv/common/recording/RecordingCapability.java index 266fd271..9fef527c 100644 --- a/common/src/com/android/tv/common/recording/RecordingCapability.java +++ b/common/src/com/android/tv/common/recording/RecordingCapability.java @@ -18,16 +18,11 @@ package com.android.tv.common.recording; import android.os.Parcel; import android.os.Parcelable; - import java.util.Objects; -/** - * Static representation of the recording capability of a TvInputService. - */ -public final class RecordingCapability implements Parcelable{ - /** - * The inputId this capability represents. - */ +/** Static representation of the recording capability of a TvInputService. */ +public final class RecordingCapability implements Parcelable { + /** The inputId this capability represents. */ public final String inputId; /** @@ -40,8 +35,8 @@ public final class RecordingCapability implements Parcelable{ /** * The max number concurrent session that play a stream. * - *<p>This is often limited by the number of decoders available. - * The count includes both playing live TV and playing a recorded stream. + * <p>This is often limited by the number of decoders available. The count includes both playing + * live TV and playing a recorded stream. */ public final int maxConcurrentPlayingSessions; @@ -52,13 +47,14 @@ public final class RecordingCapability implements Parcelable{ */ public final int maxConcurrentSessionsOfAllTypes; - /** - * True if a tuned session can support recording and playback from the same resource. - */ + /** True if a tuned session can support recording and playback from the same resource. */ public final boolean playbackWhileRecording; - private RecordingCapability(String inputId, int maxConcurrentTunedSessions, - int maxConcurrentPlayingSessions, int maxConcurrentSessionsOfAllTypes, + private RecordingCapability( + String inputId, + int maxConcurrentTunedSessions, + int maxConcurrentPlayingSessions, + int maxConcurrentSessionsOfAllTypes, boolean playbackWhileRecording) { this.inputId = inputId; this.maxConcurrentTunedSessions = maxConcurrentTunedSessions; @@ -93,12 +89,12 @@ public final class RecordingCapability implements Parcelable{ return false; } RecordingCapability that = (RecordingCapability) o; - return Objects.equals(maxConcurrentTunedSessions, that.maxConcurrentTunedSessions) && - Objects.equals(maxConcurrentPlayingSessions, that.maxConcurrentPlayingSessions) && - Objects.equals(maxConcurrentSessionsOfAllTypes, - that.maxConcurrentSessionsOfAllTypes) && - Objects.equals(playbackWhileRecording, that.playbackWhileRecording) && - Objects.equals(inputId, that.inputId); + return Objects.equals(maxConcurrentTunedSessions, that.maxConcurrentTunedSessions) + && Objects.equals(maxConcurrentPlayingSessions, that.maxConcurrentPlayingSessions) + && Objects.equals( + maxConcurrentSessionsOfAllTypes, that.maxConcurrentSessionsOfAllTypes) + && Objects.equals(playbackWhileRecording, that.playbackWhileRecording) + && Objects.equals(inputId, that.inputId); } @Override @@ -108,13 +104,19 @@ public final class RecordingCapability implements Parcelable{ @Override public String toString() { - return "RecordingCapability{" + - "inputId='" + inputId + '\'' + - ", maxConcurrentTunedSessions=" + maxConcurrentTunedSessions + - ", maxConcurrentPlayingSessions=" + maxConcurrentPlayingSessions + - ", maxConcurrentSessionsOfAllTypes=" + maxConcurrentSessionsOfAllTypes + - ", playbackWhileRecording=" + playbackWhileRecording + - '}'; + return "RecordingCapability{" + + "inputId='" + + inputId + + '\'' + + ", maxConcurrentTunedSessions=" + + maxConcurrentTunedSessions + + ", maxConcurrentPlayingSessions=" + + maxConcurrentPlayingSessions + + ", maxConcurrentSessionsOfAllTypes=" + + maxConcurrentSessionsOfAllTypes + + ", playbackWhileRecording=" + + playbackWhileRecording + + '}'; } @Override @@ -122,17 +124,18 @@ public final class RecordingCapability implements Parcelable{ return 0; } - public static final Creator<RecordingCapability> CREATOR = new Creator<RecordingCapability>() { - @Override - public RecordingCapability createFromParcel(Parcel in) { - return new RecordingCapability(in); - } + public static final Creator<RecordingCapability> CREATOR = + new Creator<RecordingCapability>() { + @Override + public RecordingCapability createFromParcel(Parcel in) { + return new RecordingCapability(in); + } - @Override - public RecordingCapability[] newArray(int size) { - return new RecordingCapability[size]; - } - }; + @Override + public RecordingCapability[] newArray(int size) { + return new RecordingCapability[size]; + } + }; public static Builder builder() { return new Builder(); @@ -171,11 +174,12 @@ public final class RecordingCapability implements Parcelable{ } public RecordingCapability build() { - return new RecordingCapability(mInputId, mMaxConcurrentTunedSessions, - mMaxConcurrentPlayingSessions, mMaxConcurrentSessionsOfAllTypes, + return new RecordingCapability( + mInputId, + mMaxConcurrentTunedSessions, + mMaxConcurrentPlayingSessions, + mMaxConcurrentSessionsOfAllTypes, mPlaybackWhileRecording); } } } - - diff --git a/common/src/com/android/tv/common/recording/RecordingStorageStatusManager.java b/common/src/com/android/tv/common/recording/RecordingStorageStatusManager.java new file mode 100644 index 00000000..8b45a730 --- /dev/null +++ b/common/src/com/android/tv/common/recording/RecordingStorageStatusManager.java @@ -0,0 +1,256 @@ +/* + * Copyright (C) 2016 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 + */ + +package com.android.tv.common.recording; + +import android.content.BroadcastReceiver; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Environment; +import android.os.Looper; +import android.os.StatFs; +import android.support.annotation.AnyThread; +import android.support.annotation.IntDef; +import android.support.annotation.WorkerThread; +import android.util.Log; +import com.android.tv.common.SoftPreconditions; +import com.android.tv.common.feature.CommonFeatures; +import java.io.File; +import java.io.IOException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; + +/** Signals DVR storage status change such as plugging/unplugging. */ +public class RecordingStorageStatusManager { + private static final String TAG = "RecordingStorageStatusManager"; + private static final boolean DEBUG = false; + + /** Minimum storage size to support DVR */ + public static final long MIN_STORAGE_SIZE_FOR_DVR_IN_BYTES = 50 * 1024 * 1024 * 1024L; // 50GB + + private static final long MIN_FREE_STORAGE_SIZE_FOR_DVR_IN_BYTES = + 10 * 1024 * 1024 * 1024L; // 10GB + private static final String RECORDING_DATA_SUB_PATH = "/recording"; + + /** Storage status constants. */ + @IntDef({ + STORAGE_STATUS_OK, + STORAGE_STATUS_TOTAL_CAPACITY_TOO_SMALL, + STORAGE_STATUS_FREE_SPACE_INSUFFICIENT, + STORAGE_STATUS_MISSING + }) + @Retention(RetentionPolicy.SOURCE) + public @interface StorageStatus {} + + /** Current storage is OK to record a program. */ + public static final int STORAGE_STATUS_OK = 0; + + /** Current storage's total capacity is smaller than DVR requirement. */ + public static final int STORAGE_STATUS_TOTAL_CAPACITY_TOO_SMALL = 1; + + /** Current storage's free space is insufficient to record programs. */ + public static final int STORAGE_STATUS_FREE_SPACE_INSUFFICIENT = 2; + + /** Current storage is missing. */ + public static final int STORAGE_STATUS_MISSING = 3; + + private final Context mContext; + private final Set<OnStorageMountChangedListener> mOnStorageMountChangedListeners = + new CopyOnWriteArraySet<>(); + private MountedStorageStatus mMountedStorageStatus; + private boolean mStorageValid; + + private class MountedStorageStatus { + private final boolean mStorageMounted; + private final File mStorageMountedDir; + private final long mStorageMountedCapacity; + + private MountedStorageStatus(boolean mounted, File mountedDir, long capacity) { + mStorageMounted = mounted; + mStorageMountedDir = mountedDir; + mStorageMountedCapacity = capacity; + } + + private boolean isValidForDvr() { + return mStorageMounted && mStorageMountedCapacity >= MIN_STORAGE_SIZE_FOR_DVR_IN_BYTES; + } + + @Override + public boolean equals(Object other) { + if (!(other instanceof MountedStorageStatus)) { + return false; + } + MountedStorageStatus status = (MountedStorageStatus) other; + return mStorageMounted == status.mStorageMounted + && Objects.equals(mStorageMountedDir, status.mStorageMountedDir) + && mStorageMountedCapacity == status.mStorageMountedCapacity; + } + } + + public interface OnStorageMountChangedListener { + + /** + * Listener for DVR storage status change. + * + * @param storageMounted {@code true} when DVR possible storage is mounted, {@code false} + * otherwise. + */ + void onStorageMountChanged(boolean storageMounted); + } + + private final class StorageStatusBroadcastReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + MountedStorageStatus result = getStorageStatusInternal(); + if (mMountedStorageStatus.equals(result)) { + return; + } + mMountedStorageStatus = result; + if (result.mStorageMounted) { + cleanUpDbIfNeeded(); + } + boolean valid = result.isValidForDvr(); + if (valid == mStorageValid) { + return; + } + mStorageValid = valid; + for (OnStorageMountChangedListener l : mOnStorageMountChangedListeners) { + l.onStorageMountChanged(valid); + } + } + } + + /** + * Creates RecordingStorageStatusManager. + * + * @param context {@link Context} + */ + public RecordingStorageStatusManager(final Context context) { + mContext = context; + mMountedStorageStatus = getStorageStatusInternal(); + mStorageValid = mMountedStorageStatus.isValidForDvr(); + IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_MEDIA_MOUNTED); + filter.addAction(Intent.ACTION_MEDIA_UNMOUNTED); + filter.addAction(Intent.ACTION_MEDIA_EJECT); + filter.addAction(Intent.ACTION_MEDIA_REMOVED); + filter.addAction(Intent.ACTION_MEDIA_BAD_REMOVAL); + filter.addDataScheme(ContentResolver.SCHEME_FILE); + mContext.registerReceiver(new StorageStatusBroadcastReceiver(), filter); + } + + /** + * Adds the listener for receiving storage status change. + * + * @param listener + */ + public void addListener(OnStorageMountChangedListener listener) { + mOnStorageMountChangedListeners.add(listener); + } + + /** Removes the current listener. */ + public void removeListener(OnStorageMountChangedListener listener) { + mOnStorageMountChangedListeners.remove(listener); + } + + /** Returns true if a storage is mounted. */ + public boolean isStorageMounted() { + return mMountedStorageStatus.mStorageMounted; + } + + /** Returns the path to DVR recording data directory. This can take for a while sometimes. */ + @WorkerThread + public File getRecordingRootDataDirectory() { + SoftPreconditions.checkState(Looper.myLooper() != Looper.getMainLooper()); + if (mMountedStorageStatus.mStorageMountedDir == null) { + return null; + } + File root = mContext.getExternalFilesDir(null); + String rootPath; + try { + rootPath = root != null ? root.getCanonicalPath() : null; + } catch (IOException | SecurityException e) { + return null; + } + return rootPath == null ? null : new File(rootPath + RECORDING_DATA_SUB_PATH); + } + + /** + * Returns the current storage status for DVR recordings. + * + * @return {@link StorageStatus} + */ + @AnyThread + public @StorageStatus int getDvrStorageStatus() { + MountedStorageStatus status = mMountedStorageStatus; + if (status.mStorageMountedDir == null) { + return STORAGE_STATUS_MISSING; + } + if (CommonFeatures.FORCE_RECORDING_UNTIL_NO_SPACE.isEnabled(mContext)) { + return STORAGE_STATUS_OK; + } + if (status.mStorageMountedCapacity < MIN_STORAGE_SIZE_FOR_DVR_IN_BYTES) { + return STORAGE_STATUS_TOTAL_CAPACITY_TOO_SMALL; + } + try { + StatFs statFs = new StatFs(status.mStorageMountedDir.toString()); + if (statFs.getAvailableBytes() < MIN_FREE_STORAGE_SIZE_FOR_DVR_IN_BYTES) { + return STORAGE_STATUS_FREE_SPACE_INSUFFICIENT; + } + } catch (IllegalArgumentException e) { + // In rare cases, storage status change was not notified yet. + SoftPreconditions.checkState(false); + return STORAGE_STATUS_FREE_SPACE_INSUFFICIENT; + } + return STORAGE_STATUS_OK; + } + + /** + * Returns whether the storage has sufficient storage. + * + * @return {@code true} when there is sufficient storage, {@code false} otherwise + */ + public boolean isStorageSufficient() { + return getDvrStorageStatus() == STORAGE_STATUS_OK; + } + + /** APPs that want to clean up DB for recordings should override this method to do the job. */ + protected void cleanUpDbIfNeeded() {} + + private MountedStorageStatus getStorageStatusInternal() { + boolean storageMounted = + Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED); + File storageMountedDir = storageMounted ? Environment.getExternalStorageDirectory() : null; + storageMounted = storageMounted && storageMountedDir != null; + long storageMountedCapacity = 0L; + if (storageMounted) { + try { + StatFs statFs = new StatFs(storageMountedDir.toString()); + storageMountedCapacity = statFs.getTotalBytes(); + } catch (IllegalArgumentException e) { + Log.e(TAG, "Storage mount status was changed."); + storageMounted = false; + storageMountedDir = null; + } + } + return new MountedStorageStatus(storageMounted, storageMountedDir, storageMountedCapacity); + } +} diff --git a/common/src/com/android/tv/common/ui/setup/OnActionClickListener.java b/common/src/com/android/tv/common/ui/setup/OnActionClickListener.java index 392d489f..6f088c0b 100644 --- a/common/src/com/android/tv/common/ui/setup/OnActionClickListener.java +++ b/common/src/com/android/tv/common/ui/setup/OnActionClickListener.java @@ -18,14 +18,12 @@ package com.android.tv.common.ui.setup; import android.os.Bundle; -/** - * A listener for the action click. - */ +/** A listener for the action click. */ public interface OnActionClickListener { /** * Called when the action is clicked. - * <p> - * The method should return {@code true} if the action is handled, otherwise {@code false}. + * + * <p>The method should return {@code true} if the action is handled, otherwise {@code false}. * * @param category The action category. * @param id The action id. diff --git a/common/src/com/android/tv/common/ui/setup/SetupActionHelper.java b/common/src/com/android/tv/common/ui/setup/SetupActionHelper.java index 7ee06faf..8a7dbd70 100644 --- a/common/src/com/android/tv/common/ui/setup/SetupActionHelper.java +++ b/common/src/com/android/tv/common/ui/setup/SetupActionHelper.java @@ -22,46 +22,45 @@ import android.util.Log; import android.view.View; import android.view.View.OnClickListener; -/** - * Helper class for the execution in the fragment. - */ +/** Helper class for the execution in the fragment. */ public class SetupActionHelper { private static final String TAG = "SetupActionHelper"; - /** - * Executes the action. - */ + /** Executes the action. */ public static boolean onActionClick(Fragment fragment, String category, int actionId) { return onActionClick(fragment, category, actionId, null); } - /** - * Executes the action. - */ - public static boolean onActionClick(Fragment fragment, String category, int actionId, - Bundle params) { + /** Executes the action. */ + public static boolean onActionClick( + Fragment fragment, String category, int actionId, Bundle params) { if (fragment.getActivity() instanceof OnActionClickListener) { - return ((OnActionClickListener) fragment.getActivity()).onActionClick(category, - actionId, params); + return ((OnActionClickListener) fragment.getActivity()) + .onActionClick(category, actionId, params); } - Log.e(TAG, "Activity can't handle the action: {category=" + category + ", actionId=" - + actionId + ", params=" + params + "}"); + Log.e( + TAG, + "Activity can't handle the action: {category=" + + category + + ", actionId=" + + actionId + + ", params=" + + params + + "}"); return false; } - /** - * Creates an {@link OnClickListener} to handle the action. - */ - public static OnClickListener createOnClickListenerForAction(Fragment fragment, String category, - int actionId, Bundle params) { + /** Creates an {@link OnClickListener} to handle the action. */ + public static OnClickListener createOnClickListenerForAction( + Fragment fragment, String category, int actionId, Bundle params) { return new OnActionClickListenerForAction(fragment, category, actionId, params); } /** * The {@link OnClickListener} for the view. - * <p> - * Note that this class should be used only for the views in the {@code mFragment} to avoid the - * leak of mFragment. + * + * <p>Note that this class should be used only for the views in the {@code mFragment} to avoid + * the leak of mFragment. */ private static class OnActionClickListenerForAction implements OnClickListener { private final Fragment mFragment; @@ -69,8 +68,8 @@ public class SetupActionHelper { private final int mActionId; private final Bundle mParams; - OnActionClickListenerForAction(Fragment fragment, String category, int actionId, - Bundle params) { + OnActionClickListenerForAction( + Fragment fragment, String category, int actionId, Bundle params) { mFragment = fragment; mCategory = category; mActionId = actionId; @@ -83,5 +82,5 @@ public class SetupActionHelper { } } - private SetupActionHelper() { } + private SetupActionHelper() {} } diff --git a/common/src/com/android/tv/common/ui/setup/SetupActivity.java b/common/src/com/android/tv/common/ui/setup/SetupActivity.java index 2b381a6e..67418ce0 100644 --- a/common/src/com/android/tv/common/ui/setup/SetupActivity.java +++ b/common/src/com/android/tv/common/ui/setup/SetupActivity.java @@ -28,7 +28,6 @@ import android.transition.Transition; import android.transition.TransitionInflater; import android.view.View; import android.view.ViewTreeObserver.OnPreDrawListener; - import com.android.tv.common.R; import com.android.tv.common.WeakHandler; import com.android.tv.common.ui.setup.animation.SetupAnimationHelper; @@ -51,23 +50,28 @@ public abstract class SetupActivity extends Activity implements OnActionClickLis super.onCreate(savedInstanceState); SetupAnimationHelper.initialize(this); setContentView(R.layout.activity_setup); - mFragmentTransitionDuration = getResources().getInteger( - R.integer.setup_fragment_transition_duration); + mFragmentTransitionDuration = + getResources().getInteger(R.integer.setup_fragment_transition_duration); // Show initial fragment only when the saved state is not restored, because the last // fragment is restored if savesInstanceState is not null. if (savedInstanceState == null) { // This is the workaround to show the first fragment with delay to show the fragment // enter transition. See http://b/26255145 - getWindow().getDecorView().getViewTreeObserver().addOnPreDrawListener( - new OnPreDrawListener() { - @Override - public boolean onPreDraw() { - getWindow().getDecorView().getViewTreeObserver() - .removeOnPreDrawListener(this); - showInitialFragment(); - return true; - } - }); + getWindow() + .getDecorView() + .getViewTreeObserver() + .addOnPreDrawListener( + new OnPreDrawListener() { + @Override + public boolean onPreDraw() { + getWindow() + .getDecorView() + .getViewTreeObserver() + .removeOnPreDrawListener(this); + showInitialFragment(); + return true; + } + }); } else { mShowInitialFragment = false; } @@ -76,8 +80,8 @@ public abstract class SetupActivity extends Activity implements OnActionClickLis /** * The inherited class should provide the initial fragment to show. * - * <p>If this method returns {@code null} during {@link #onCreate}, then call - * {@link #showInitialFragment} explicitly later with non null initial fragment. + * <p>If this method returns {@code null} during {@link #onCreate}, then call {@link + * #showInitialFragment} explicitly later with non null initial fragment. */ protected abstract Fragment onCreateInitialFragment(); @@ -98,16 +102,15 @@ public abstract class SetupActivity extends Activity implements OnActionClickLis } } - /** - * Shows the given fragment. - */ + /** Shows the given fragment. */ protected FragmentTransaction showFragment(Fragment fragment, boolean addToBackStack) { FragmentTransaction ft = getFragmentManager().beginTransaction(); if (fragment instanceof SetupFragment) { int[] sharedElements = ((SetupFragment) fragment).getSharedElementIds(); if (sharedElements != null && sharedElements.length > 0) { - Transition sharedTransition = TransitionInflater.from(this) - .inflateTransition(R.transition.transition_action_background); + Transition sharedTransition = + TransitionInflater.from(this) + .inflateTransition(R.transition.transition_action_background); sharedTransition.setDuration(getSharedElementTransitionDuration()); SetupAnimationHelper.applyAnimationTimeScale(sharedTransition); fragment.setSharedElementEnterTransition(sharedTransition); @@ -143,9 +146,9 @@ public abstract class SetupActivity extends Activity implements OnActionClickLis /** * Override this method if the inherited class wants to handle the action. - * <p> - * The override method should return {@code true} if the action is handled, otherwise - * {@code false}. + * + * <p>The override method should return {@code true} if the action is handled, otherwise {@code + * false}. */ protected boolean executeAction(String category, int actionId, Bundle params) { return false; diff --git a/common/src/com/android/tv/common/ui/setup/SetupFragment.java b/common/src/com/android/tv/common/ui/setup/SetupFragment.java index d2b9d7c8..7d47548d 100644 --- a/common/src/com/android/tv/common/ui/setup/SetupFragment.java +++ b/common/src/com/android/tv/common/ui/setup/SetupFragment.java @@ -26,22 +26,25 @@ import android.view.LayoutInflater; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; - import com.android.tv.common.ui.setup.animation.FadeAndShortSlide; import com.android.tv.common.ui.setup.animation.SetupAnimationHelper; - import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; -/** - * A fragment which slides when it is entering/exiting. - */ +/** A fragment which slides when it is entering/exiting. */ public abstract class SetupFragment extends Fragment { @Retention(RetentionPolicy.SOURCE) - @IntDef(flag = true, - value = {FRAGMENT_ENTER_TRANSITION, FRAGMENT_EXIT_TRANSITION, - FRAGMENT_REENTER_TRANSITION, FRAGMENT_RETURN_TRANSITION}) + @IntDef( + flag = true, + value = { + FRAGMENT_ENTER_TRANSITION, + FRAGMENT_EXIT_TRANSITION, + FRAGMENT_REENTER_TRANSITION, + FRAGMENT_RETURN_TRANSITION + } + ) public @interface FragmentTransitionType {} + public static final int FRAGMENT_ENTER_TRANSITION = 0x01; public static final int FRAGMENT_EXIT_TRANSITION = FRAGMENT_ENTER_TRANSITION << 1; public static final int FRAGMENT_REENTER_TRANSITION = FRAGMENT_ENTER_TRANSITION << 2; @@ -49,50 +52,50 @@ public abstract class SetupFragment extends Fragment { private boolean mEnterTransitionRunning; - private final TransitionListener mTransitionListener = new TransitionListener() { - @Override - public void onTransitionStart(Transition transition) { - mEnterTransitionRunning = true; - } + private final TransitionListener mTransitionListener = + new TransitionListener() { + @Override + public void onTransitionStart(Transition transition) { + mEnterTransitionRunning = true; + } - @Override - public void onTransitionEnd(Transition transition) { - mEnterTransitionRunning = false; - onEnterTransitionEnd(); - } + @Override + public void onTransitionEnd(Transition transition) { + mEnterTransitionRunning = false; + onEnterTransitionEnd(); + } - @Override - public void onTransitionCancel(Transition transition) { } + @Override + public void onTransitionCancel(Transition transition) {} - @Override - public void onTransitionPause(Transition transition) { } + @Override + public void onTransitionPause(Transition transition) {} - @Override - public void onTransitionResume(Transition transition) { } - }; + @Override + public void onTransitionResume(Transition transition) {} + }; - /** - * Returns {@code true} if the enter/reenter transition is running. - */ + /** Returns {@code true} if the enter/reenter transition is running. */ protected boolean isEnterTransitionRunning() { return mEnterTransitionRunning; } - /** - * Called when the enter/reenter transition ends. - */ - protected void onEnterTransitionEnd() { } + /** Called when the enter/reenter transition ends. */ + protected void onEnterTransitionEnd() {} public SetupFragment() { setAllowEnterTransitionOverlap(false); setAllowReturnTransitionOverlap(false); - enableFragmentTransition(FRAGMENT_ENTER_TRANSITION | FRAGMENT_EXIT_TRANSITION - | FRAGMENT_REENTER_TRANSITION | FRAGMENT_RETURN_TRANSITION); + enableFragmentTransition( + FRAGMENT_ENTER_TRANSITION + | FRAGMENT_EXIT_TRANSITION + | FRAGMENT_REENTER_TRANSITION + | FRAGMENT_RETURN_TRANSITION); } @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(getLayoutResourceId(), container, false); // After the transition animation, we need to request the focus. If not, this fragment // doesn't have the focus. @@ -100,18 +103,17 @@ public abstract class SetupFragment extends Fragment { return view; } - /** - * Returns the layout resource ID for this fragment. - */ - abstract protected int getLayoutResourceId(); + /** Returns the layout resource ID for this fragment. */ + protected abstract int getLayoutResourceId(); protected void setOnClickAction(View view, final String category, final int actionId) { - view.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View view) { - onActionClick(category, actionId); - } - }); + view.setOnClickListener( + new OnClickListener() { + @Override + public void onClick(View view) { + onActionClick(category, actionId); + } + }); } protected boolean onActionClick(String category, int actionId) { @@ -141,24 +143,22 @@ public abstract class SetupFragment extends Fragment { /** * Enables fragment transition according to the given {@code mask}. * - * @param mask This value is the combination of {@link #FRAGMENT_ENTER_TRANSITION}, - * {@link #FRAGMENT_EXIT_TRANSITION}, {@link #FRAGMENT_REENTER_TRANSITION}, and - * {@link #FRAGMENT_RETURN_TRANSITION}. + * @param mask This value is the combination of {@link #FRAGMENT_ENTER_TRANSITION}, {@link + * #FRAGMENT_EXIT_TRANSITION}, {@link #FRAGMENT_REENTER_TRANSITION}, and {@link + * #FRAGMENT_RETURN_TRANSITION}. */ public void enableFragmentTransition(@FragmentTransitionType int mask) { - setEnterTransition((mask & FRAGMENT_ENTER_TRANSITION) == 0 ? null - : createTransition(Gravity.END)); - setExitTransition((mask & FRAGMENT_EXIT_TRANSITION) == 0 ? null - : createTransition(Gravity.START)); - setReenterTransition((mask & FRAGMENT_REENTER_TRANSITION) == 0 ? null - : createTransition(Gravity.START)); - setReturnTransition((mask & FRAGMENT_RETURN_TRANSITION) == 0 ? null - : createTransition(Gravity.END)); + setEnterTransition( + (mask & FRAGMENT_ENTER_TRANSITION) == 0 ? null : createTransition(Gravity.END)); + setExitTransition( + (mask & FRAGMENT_EXIT_TRANSITION) == 0 ? null : createTransition(Gravity.START)); + setReenterTransition( + (mask & FRAGMENT_REENTER_TRANSITION) == 0 ? null : createTransition(Gravity.START)); + setReturnTransition( + (mask & FRAGMENT_RETURN_TRANSITION) == 0 ? null : createTransition(Gravity.END)); } - /** - * Sets the transition with the given {@code slidEdge}. - */ + /** Sets the transition with the given {@code slidEdge}. */ public void setFragmentTransition(@FragmentTransitionType int transitionType, int slideEdge) { switch (transitionType) { case FRAGMENT_ENTER_TRANSITION: @@ -184,9 +184,7 @@ public abstract class SetupFragment extends Fragment { .build(); } - /** - * Changes the move distance of the transitions to short distance. - */ + /** Changes the move distance of the transitions to short distance. */ public void setShortDistance(@FragmentTransitionType int mask) { if ((mask & FRAGMENT_ENTER_TRANSITION) != 0) { Transition transition = getEnterTransition(); @@ -218,7 +216,7 @@ public abstract class SetupFragment extends Fragment { * Returns the ID's of the view's whose descendants will perform delayed move. * * @see com.android.tv.common.ui.setup.animation.SetupAnimationHelper.TransitionBuilder - * #setParentIdsForDelay + * #setParentIdsForDelay */ protected int[] getParentIdsForDelay() { return null; @@ -228,7 +226,7 @@ public abstract class SetupFragment extends Fragment { * Sets the ID's of the views which will not be included in the transition. * * @see com.android.tv.common.ui.setup.animation.SetupAnimationHelper.TransitionBuilder - * #setExcludeIds + * #setExcludeIds */ protected int[] getExcludedTargetIds() { return null; diff --git a/common/src/com/android/tv/common/ui/setup/SetupGuidedStepFragment.java b/common/src/com/android/tv/common/ui/setup/SetupGuidedStepFragment.java index 88159da9..3c76c269 100644 --- a/common/src/com/android/tv/common/ui/setup/SetupGuidedStepFragment.java +++ b/common/src/com/android/tv/common/ui/setup/SetupGuidedStepFragment.java @@ -16,22 +16,26 @@ package com.android.tv.common.ui.setup; +import static android.content.Context.ACCESSIBILITY_SERVICE; + import android.os.Bundle; import android.support.v17.leanback.app.GuidedStepFragment; import android.support.v17.leanback.widget.GuidanceStylist; import android.support.v17.leanback.widget.GuidedAction; +import android.support.v17.leanback.widget.GuidedActionsStylist; import android.support.v17.leanback.widget.VerticalGridView; import android.view.LayoutInflater; import android.view.View; +import android.view.View.AccessibilityDelegate; import android.view.ViewGroup; import android.view.ViewGroup.MarginLayoutParams; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityManager; +import android.view.accessibility.AccessibilityNodeInfo; import android.widget.LinearLayout; - import com.android.tv.common.R; -/** - * A fragment for channel source info/setup. - */ +/** A fragment for channel source info/setup. */ public abstract class SetupGuidedStepFragment extends GuidedStepFragment { /** * Key of the argument which indicate whether the parent of this fragment has three panes. @@ -40,23 +44,32 @@ public abstract class SetupGuidedStepFragment extends GuidedStepFragment { */ public static final String KEY_THREE_PANE = "key_three_pane"; + private View mContentFragment; + private boolean mFromContentFragment; + private boolean mAccessibilityMode; + @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = super.onCreateView(inflater, container, savedInstanceState); Bundle arguments = getArguments(); - view.findViewById(R.id.action_fragment_root).setPadding(0, 0, 0, 0); - LinearLayout.LayoutParams guidanceLayoutParams = (LinearLayout.LayoutParams) - view.findViewById(R.id.content_fragment).getLayoutParams(); + view.findViewById(android.support.v17.leanback.R.id.action_fragment_root) + .setPadding(0, 0, 0, 0); + mContentFragment = view.findViewById(android.support.v17.leanback.R.id.content_fragment); + LinearLayout.LayoutParams guidanceLayoutParams = + (LinearLayout.LayoutParams) mContentFragment.getLayoutParams(); guidanceLayoutParams.weight = 0; if (arguments != null && arguments.getBoolean(KEY_THREE_PANE, false)) { // Content fragment. - guidanceLayoutParams.width = getResources().getDimensionPixelOffset( - R.dimen.setup_guidedstep_guidance_section_width_3pane); - int doneButtonWidth = getResources().getDimensionPixelOffset( - R.dimen.setup_done_button_container_width); + guidanceLayoutParams.width = + getResources() + .getDimensionPixelOffset( + R.dimen.setup_guidedstep_guidance_section_width_3pane); + int doneButtonWidth = + getResources() + .getDimensionPixelOffset(R.dimen.setup_done_button_container_width); // Guided actions list - View list = view.findViewById(R.id.guidedactions_list); + View list = view.findViewById(android.support.v17.leanback.R.id.guidedactions_list); MarginLayoutParams marginLayoutParams = (MarginLayoutParams) list.getLayoutParams(); // Use content view to check layout direction while view is being created. if (getResources().getConfiguration().getLayoutDirection() @@ -67,31 +80,68 @@ public abstract class SetupGuidedStepFragment extends GuidedStepFragment { } } else { // Content fragment. - guidanceLayoutParams.width = getResources().getDimensionPixelOffset( - R.dimen.setup_guidedstep_guidance_section_width_2pane); + guidanceLayoutParams.width = + getResources() + .getDimensionPixelOffset( + R.dimen.setup_guidedstep_guidance_section_width_2pane); } // gridView Alignment VerticalGridView gridView = getGuidedActionsStylist().getActionsGridView(); - int offset = getResources().getDimensionPixelOffset( - R.dimen.setup_guidedactions_selector_margin_top); + int offset = + getResources() + .getDimensionPixelOffset(R.dimen.setup_guidedactions_selector_margin_top); gridView.setWindowAlignmentOffset(offset); gridView.setWindowAlignmentOffsetPercent(0); gridView.setItemAlignmentOffsetPercent(0); - ((ViewGroup) view.findViewById(R.id.guidedactions_list)).setTransitionGroup(false); + ((ViewGroup) view.findViewById(android.support.v17.leanback.R.id.guidedactions_list)) + .setTransitionGroup(false); // Needed for the shared element transition. // content_frame is defined in leanback. - ViewGroup group = (ViewGroup) view.findViewById(R.id.content_frame); + ViewGroup group = + (ViewGroup) view.findViewById(android.support.v17.leanback.R.id.content_frame); group.setClipChildren(false); group.setClipToPadding(false); return view; } @Override + public GuidedActionsStylist onCreateActionsStylist() { + return new SetupGuidedStepFragmentGuidedActionsStylist(); + } + + @Override + public void onResume() { + super.onResume(); + AccessibilityManager am = + (AccessibilityManager) getActivity().getSystemService(ACCESSIBILITY_SERVICE); + mAccessibilityMode = am != null && am.isEnabled() && am.isTouchExplorationEnabled(); + mContentFragment.setFocusable(mAccessibilityMode); + if (mAccessibilityMode) { + mContentFragment.setAccessibilityDelegate( + new AccessibilityDelegate() { + @Override + public boolean performAccessibilityAction(View host, int action, Bundle args) { + if (action == AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS + && !getActions().isEmpty()) { + // scroll to the top. This makes the first action view on the screen. + // Otherwise, the view can be recycled, so accessibility events cannot + // be sent later. + getGuidedActionsStylist().getActionsGridView().scrollToPosition(0); + mFromContentFragment = true; + } + return super.performAccessibilityAction(host, action, args); + } + }); + mContentFragment.requestFocus(); + } + } + + @Override public GuidanceStylist onCreateGuidanceStylist() { return new GuidanceStylist() { @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Guidance guidance) { + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Guidance guidance) { View view = super.onCreateView(inflater, container, guidance); if (guidance.getIconDrawable() == null) { // Icon view should not take up space when we don't use image. @@ -102,10 +152,19 @@ public abstract class SetupGuidedStepFragment extends GuidedStepFragment { }; } - abstract protected String getActionCategory(); + protected abstract String getActionCategory(); + + protected View getDoneButton() { + return getActivity().findViewById(R.id.button_done); + } @Override public void onGuidedActionClicked(GuidedAction action) { + if (!action.isFocusable()) { + // an unfocusable action may be clicked in accessibility mode when it's accessibility + // focused + return; + } SetupActionHelper.onActionClick(this, getActionCategory(), (int) action.getId()); } @@ -118,4 +177,38 @@ public abstract class SetupGuidedStepFragment extends GuidedStepFragment { public boolean isFocusOutEndAllowed() { return true; } + + protected void setAccessibilityDelegate(GuidedActionsStylist.ViewHolder vh, + GuidedAction action) { + if (!mAccessibilityMode || findActionPositionById(action.getId()) == 0) { + return; + } + vh.itemView.setAccessibilityDelegate( + new AccessibilityDelegate() { + @Override + public boolean performAccessibilityAction(View host, int action, Bundle args) { + if ((action == AccessibilityNodeInfo.ACTION_FOCUS + || action == AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS) + && mFromContentFragment) { + // block the action and make the first action view accessibility focused + View view = getActionItemView(0); + if (view != null) { + view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED); + mFromContentFragment = false; + return true; + } + } + return super.performAccessibilityAction(host, action, args); + } + }); + } + + private class SetupGuidedStepFragmentGuidedActionsStylist extends GuidedActionsStylist { + + @Override + public void onBindViewHolder(GuidedActionsStylist.ViewHolder vh, GuidedAction action) { + super.onBindViewHolder(vh, action); + setAccessibilityDelegate(vh, action); + } + } } diff --git a/common/src/com/android/tv/common/ui/setup/SetupMultiPaneFragment.java b/common/src/com/android/tv/common/ui/setup/SetupMultiPaneFragment.java index b9ad4657..c02d3f56 100644 --- a/common/src/com/android/tv/common/ui/setup/SetupMultiPaneFragment.java +++ b/common/src/com/android/tv/common/ui/setup/SetupMultiPaneFragment.java @@ -23,34 +23,43 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.ViewGroup.MarginLayoutParams; - import com.android.tv.common.R; -/** - * A fragment for channel source info/setup. - */ +/** A fragment for channel source info/setup. */ public abstract class SetupMultiPaneFragment extends SetupFragment { private static final String TAG = "SetupMultiPaneFragment"; private static final boolean DEBUG = false; public static final int ACTION_DONE = Integer.MAX_VALUE; public static final int ACTION_SKIP = ACTION_DONE - 1; + public static final int MAX_SUBCLASSES_ID = ACTION_SKIP - 1; - private static final String CONTENT_FRAGMENT_TAG = "content_fragment"; + public static final String CONTENT_FRAGMENT_TAG = "content_fragment"; @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { if (DEBUG) { - Log.d(TAG, "onCreateView(" + inflater + ", " + container + ", " + savedInstanceState - + ")"); + Log.d( + TAG, + "onCreateView(" + + inflater + + ", " + + container + + ", " + + savedInstanceState + + ")"); } View view = super.onCreateView(inflater, container, savedInstanceState); if (savedInstanceState == null) { SetupGuidedStepFragment contentFragment = onCreateContentFragment(); - getChildFragmentManager().beginTransaction() - .replace(R.id.guided_step_fragment_container, contentFragment, - CONTENT_FRAGMENT_TAG).commit(); + getChildFragmentManager() + .beginTransaction() + .replace( + R.id.guided_step_fragment_container, + contentFragment, + CONTENT_FRAGMENT_TAG) + .commit(); } if (needsDoneButton()) { setOnClickAction(view.findViewById(R.id.button_done), getActionCategory(), ACTION_DONE); @@ -65,12 +74,12 @@ public abstract class SetupMultiPaneFragment extends SetupFragment { if (getResources().getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_LTR) { ((MarginLayoutParams) doneButtonContainer.getLayoutParams()).rightMargin = - -getResources().getDimensionPixelOffset( - R.dimen.setup_done_button_container_width); + -getResources() + .getDimensionPixelOffset(R.dimen.setup_done_button_container_width); } else { ((MarginLayoutParams) doneButtonContainer.getLayoutParams()).leftMargin = - -getResources().getDimensionPixelOffset( - R.dimen.setup_done_button_container_width); + -getResources() + .getDimensionPixelOffset(R.dimen.setup_done_button_container_width); } view.findViewById(R.id.button_done).setFocusable(false); } @@ -82,15 +91,15 @@ public abstract class SetupMultiPaneFragment extends SetupFragment { return R.layout.fragment_setup_multi_pane; } - abstract protected SetupGuidedStepFragment onCreateContentFragment(); + protected abstract SetupGuidedStepFragment onCreateContentFragment(); @Nullable protected SetupGuidedStepFragment getContentFragment() { - return (SetupGuidedStepFragment) getChildFragmentManager() - .findFragmentByTag(CONTENT_FRAGMENT_TAG); + return (SetupGuidedStepFragment) + getChildFragmentManager().findFragmentByTag(CONTENT_FRAGMENT_TAG); } - abstract protected String getActionCategory(); + protected abstract String getActionCategory(); protected boolean needsDoneButton() { return true; @@ -102,11 +111,16 @@ public abstract class SetupMultiPaneFragment extends SetupFragment { @Override protected int[] getParentIdsForDelay() { - return new int[] {R.id.content_fragment, R.id.guidedactions_list}; + return new int[] { + android.support.v17.leanback.R.id.content_fragment, + android.support.v17.leanback.R.id.guidedactions_list + }; } @Override public int[] getSharedElementIds() { - return new int[] {R.id.action_fragment_background, R.id.done_button_container}; + return new int[] { + android.support.v17.leanback.R.id.action_fragment_background, R.id.done_button_container + }; } -}
\ No newline at end of file +} diff --git a/common/src/com/android/tv/common/ui/setup/animation/FadeAndShortSlide.java b/common/src/com/android/tv/common/ui/setup/animation/FadeAndShortSlide.java index e1a8e60c..60ffb70f 100644 --- a/common/src/com/android/tv/common/ui/setup/animation/FadeAndShortSlide.java +++ b/common/src/com/android/tv/common/ui/setup/animation/FadeAndShortSlide.java @@ -29,15 +29,12 @@ import android.view.ViewGroup; import android.view.ViewParent; import android.view.animation.AccelerateInterpolator; import android.view.animation.DecelerateInterpolator; - import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; -/** - * Execute horizontal slide of 1/4 width and fade (to workaround bug 23718734) - */ +/** Execute horizontal slide of 1/4 width and fade (to workaround bug 23718734) */ public class FadeAndShortSlide extends Visibility { private static final TimeInterpolator APPEAR_INTERPOLATOR = new DecelerateInterpolator(); private static final TimeInterpolator DISAPPEAR_INTERPOLATOR = new AccelerateInterpolator(); @@ -48,39 +45,45 @@ public class FadeAndShortSlide extends Visibility { private static final int DEFAULT_DISTANCE = 200; - private static abstract class CalculateSlide { + private abstract static class CalculateSlide { /** Returns the translation value for view when it goes out of the scene */ - public abstract float getGoneX(ViewGroup sceneRoot, View view, int[] position, - int distance); + public abstract float getGoneX( + ViewGroup sceneRoot, View view, int[] position, int distance); } - private static final CalculateSlide sCalculateStart = new CalculateSlide() { - @Override - public float getGoneX(ViewGroup sceneRoot, View view, int[] position, int distance) { - final boolean isRtl = sceneRoot.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; - final float x; - if (isRtl) { - x = view.getTranslationX() + distance; - } else { - x = view.getTranslationX() - distance; - } - return x; - } - }; - - private static final CalculateSlide sCalculateEnd = new CalculateSlide() { - @Override - public float getGoneX(ViewGroup sceneRoot, View view, int[] position, int distance) { - final boolean isRtl = sceneRoot.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; - final float x; - if (isRtl) { - x = view.getTranslationX() - distance; - } else { - x = view.getTranslationX() + distance; - } - return x; - } - }; + private static final CalculateSlide sCalculateStart = + new CalculateSlide() { + @Override + public float getGoneX( + ViewGroup sceneRoot, View view, int[] position, int distance) { + final boolean isRtl = + sceneRoot.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; + final float x; + if (isRtl) { + x = view.getTranslationX() + distance; + } else { + x = view.getTranslationX() - distance; + } + return x; + } + }; + + private static final CalculateSlide sCalculateEnd = + new CalculateSlide() { + @Override + public float getGoneX( + ViewGroup sceneRoot, View view, int[] position, int distance) { + final boolean isRtl = + sceneRoot.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; + final float x; + if (isRtl) { + x = view.getTranslationX() - distance; + } else { + x = view.getTranslationX() + distance; + } + return x; + } + }; private static final ViewPositionComparator sViewPositionComparator = new ViewPositionComparator(); @@ -131,9 +134,10 @@ public class FadeAndShortSlide extends Visibility { getTransitionTargets((ViewGroup) parentForDelay, transitionTargets); sViewPositionComparator.mParentForDelay = parentForDelay; sViewPositionComparator.mIsLtr = view.getLayoutDirection() == View.LAYOUT_DIRECTION_LTR; - sViewPositionComparator.mToLeft = sViewPositionComparator.mIsLtr - ? mSlideEdge == (appear ? Gravity.END : Gravity.START) - : mSlideEdge == (appear ? Gravity.START : Gravity.END); + sViewPositionComparator.mToLeft = + sViewPositionComparator.mIsLtr + ? mSlideEdge == (appear ? Gravity.END : Gravity.START) + : mSlideEdge == (appear ? Gravity.START : Gravity.END); Collections.sort(transitionTargets, sViewPositionComparator); return transitionTargets.indexOf(view); } @@ -180,8 +184,8 @@ public class FadeAndShortSlide extends Visibility { captureValues(transitionValues); int delayIndex = getDelayOrder(transitionValues.view, false); if (delayIndex > 0) { - transitionValues.values.put(PROPNAME_DELAY, - delayIndex * SetupAnimationHelper.DELAY_BETWEEN_SIBLINGS_MS); + transitionValues.values.put( + PROPNAME_DELAY, delayIndex * SetupAnimationHelper.DELAY_BETWEEN_SIBLINGS_MS); } } @@ -192,8 +196,8 @@ public class FadeAndShortSlide extends Visibility { captureValues(transitionValues); int delayIndex = getDelayOrder(transitionValues.view, true); if (delayIndex > 0) { - transitionValues.values.put(PROPNAME_DELAY, - delayIndex * SetupAnimationHelper.DELAY_BETWEEN_SIBLINGS_MS); + transitionValues.values.put( + PROPNAME_DELAY, delayIndex * SetupAnimationHelper.DELAY_BETWEEN_SIBLINGS_MS); } } @@ -212,7 +216,10 @@ public class FadeAndShortSlide extends Visibility { } @Override - public Animator onAppear(ViewGroup sceneRoot, View view, TransitionValues startValues, + public Animator onAppear( + ViewGroup sceneRoot, + View view, + TransitionValues startValues, TransitionValues endValues) { if (endValues == null) { return null; @@ -221,15 +228,16 @@ public class FadeAndShortSlide extends Visibility { int left = position[0]; float endX = view.getTranslationX(); float startX = mSlideCalculator.getGoneX(sceneRoot, view, position, mDistance); - final Animator slideAnimator = TranslationAnimationCreator.createAnimation(view, endValues, - left, startX, endX, APPEAR_INTERPOLATOR, this); + final Animator slideAnimator = + TranslationAnimationCreator.createAnimation( + view, endValues, left, startX, endX, APPEAR_INTERPOLATOR, this); if (slideAnimator == null) { return null; } mFade.setInterpolator(APPEAR_INTERPOLATOR); final AnimatorSet set = new AnimatorSet(); set.play(slideAnimator).with(mFade.onAppear(sceneRoot, view, startValues, endValues)); - Long delay = (Long ) endValues.values.get(PROPNAME_DELAY); + Long delay = (Long) endValues.values.get(PROPNAME_DELAY); if (delay != null) { set.setStartDelay(delay); } @@ -237,7 +245,10 @@ public class FadeAndShortSlide extends Visibility { } @Override - public Animator onDisappear(ViewGroup sceneRoot, final View view, TransitionValues startValues, + public Animator onDisappear( + ViewGroup sceneRoot, + final View view, + TransitionValues startValues, TransitionValues endValues) { if (startValues == null) { return null; @@ -246,8 +257,9 @@ public class FadeAndShortSlide extends Visibility { int left = position[0]; float startX = view.getTranslationX(); float endX = mSlideCalculator.getGoneX(sceneRoot, view, position, mDistance); - final Animator slideAnimator = TranslationAnimationCreator.createAnimation(view, - startValues, left, startX, endX, DISAPPEAR_INTERPOLATOR, this); + final Animator slideAnimator = + TranslationAnimationCreator.createAnimation( + view, startValues, left, startX, endX, DISAPPEAR_INTERPOLATOR, this); if (slideAnimator == null) { // slideAnimator is null if startX == endX return null; } @@ -257,13 +269,14 @@ public class FadeAndShortSlide extends Visibility { if (fadeAnimator == null) { return null; } - fadeAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animator) { - fadeAnimator.removeListener(this); - view.setAlpha(0.0f); - } - }); + fadeAnimator.addListener( + new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animator) { + fadeAnimator.removeListener(this); + view.setAlpha(0.0f); + } + }); final AnimatorSet set = new AnimatorSet(); set.play(slideAnimator).with(fadeAnimator); @@ -300,9 +313,7 @@ public class FadeAndShortSlide extends Visibility { return super.setDuration(scaledDuration); } - /** - * Sets the moving distance in pixel. - */ + /** Sets the moving distance in pixel. */ public void setDistance(int distance) { mDistance = distance; } diff --git a/common/src/com/android/tv/common/ui/setup/animation/SetupAnimationHelper.java b/common/src/com/android/tv/common/ui/setup/animation/SetupAnimationHelper.java index d98138a2..2a913c6e 100644 --- a/common/src/com/android/tv/common/ui/setup/animation/SetupAnimationHelper.java +++ b/common/src/com/android/tv/common/ui/setup/animation/SetupAnimationHelper.java @@ -27,12 +27,9 @@ import android.transition.TransitionSet; import android.view.Gravity; import android.view.View; import android.widget.ImageView; - import com.android.tv.common.R; -/** - * A helper class for setup animation. - */ +/** A helper class for setup animation. */ public final class SetupAnimationHelper { public static final long DELAY_BETWEEN_SIBLINGS_MS = applyAnimationTimeScale(33); @@ -43,21 +40,21 @@ public final class SetupAnimationHelper { private static int sFragmentTransitionLongDistance; private static int sFragmentTransitionShortDistance; - private SetupAnimationHelper() { } + private SetupAnimationHelper() {} - /** - * Load initial parameters. This method should be called before using this class. - */ + /** Load initial parameters. This method should be called before using this class. */ public static void initialize(Context context) { if (sInitialized) { return; } - sFragmentTransitionDuration = context.getResources() - .getInteger(R.integer.setup_fragment_transition_duration); - sFragmentTransitionLongDistance = context.getResources() - .getDimensionPixelOffset(R.dimen.setup_fragment_transition_long_distance); - sFragmentTransitionShortDistance = context.getResources() - .getDimensionPixelOffset(R.dimen.setup_fragment_transition_short_distance); + sFragmentTransitionDuration = + context.getResources().getInteger(R.integer.setup_fragment_transition_duration); + sFragmentTransitionLongDistance = + context.getResources() + .getDimensionPixelOffset(R.dimen.setup_fragment_transition_long_distance); + sFragmentTransitionShortDistance = + context.getResources() + .getDimensionPixelOffset(R.dimen.setup_fragment_transition_short_distance); sInitialized = true; } @@ -88,9 +85,7 @@ public final class SetupAnimationHelper { return this; } - /** - * Sets the duration of the transition. - */ + /** Sets the duration of the transition. */ public TransitionBuilder setDuration(long duration) { mDuration = duration; return this; @@ -106,17 +101,13 @@ public final class SetupAnimationHelper { return this; } - /** - * Sets the ID's of the views which will not be included in the transition. - */ + /** Sets the ID's of the views which will not be included in the transition. */ public TransitionBuilder setExcludeIds(int[] excludeIds) { mExcludeIds = excludeIds; return this; } - /** - * Builds and returns the {@link android.transition.Transition}. - */ + /** Builds and returns the {@link android.transition.Transition}. */ public Transition build() { FadeAndShortSlide transition = new FadeAndShortSlide(mSlideEdge, mParentIdForDelay); transition.setDistance(mDistance); @@ -130,25 +121,19 @@ public final class SetupAnimationHelper { } } - /** - * Changes the move distance of the {@code transition} to long distance. - */ + /** Changes the move distance of the {@code transition} to long distance. */ public static void setLongDistance(FadeAndShortSlide transition) { checkInitialized(); transition.setDistance(sFragmentTransitionLongDistance); } - /** - * Changes the move distance of the {@code transition} to short distance. - */ + /** Changes the move distance of the {@code transition} to short distance. */ public static void setShortDistance(FadeAndShortSlide transition) { checkInitialized(); transition.setDistance(sFragmentTransitionShortDistance); } - /** - * Applies the animation scale to the given {@code animator}. - */ + /** Applies the animation scale to the given {@code animator}. */ public static Animator applyAnimationTimeScale(Animator animator) { if (animator instanceof AnimatorSet) { for (Animator child : ((AnimatorSet) animator).getChildAnimations()) { @@ -162,9 +147,7 @@ public final class SetupAnimationHelper { return animator; } - /** - * Applies the animation scale to the given {@code transition}. - */ + /** Applies the animation scale to the given {@code transition}. */ public static Transition applyAnimationTimeScale(Transition transition) { if (transition instanceof TransitionSet) { TransitionSet set = (TransitionSet) transition; @@ -180,9 +163,7 @@ public final class SetupAnimationHelper { return transition; } - /** - * Applies the animation scale to the given {@code time}. - */ + /** Applies the animation scale to the given {@code time}. */ public static long applyAnimationTimeScale(long time) { return (long) (time * ANIMATION_TIME_SCALE); } @@ -197,23 +178,25 @@ public final class SetupAnimationHelper { } /** - * Returns an animator which animates the source image of the {@link ImageView} with start delay. + * Returns an animator which animates the source image of the {@link ImageView} with start + * delay. * * <p>The frame rate is 60 fps. */ - public static ObjectAnimator createFrameAnimatorWithDelay(ImageView imageView, int[] frames, - long startDelay) { + public static ObjectAnimator createFrameAnimatorWithDelay( + ImageView imageView, int[] frames, long startDelay) { ObjectAnimator animator = ObjectAnimator.ofInt(imageView, "imageResource", frames); // Make it 60 fps. animator.setDuration(frames.length * 1000 / 60); animator.setInterpolator(null); animator.setStartDelay(startDelay); - animator.setEvaluator(new TypeEvaluator<Integer>() { - @Override - public Integer evaluate(float fraction, Integer startValue, Integer endValue) { - return startValue; - } - }); + animator.setEvaluator( + new TypeEvaluator<Integer>() { + @Override + public Integer evaluate(float fraction, Integer startValue, Integer endValue) { + return startValue; + } + }); return animator; } @@ -223,19 +206,20 @@ public final class SetupAnimationHelper { * @param view The view which will be animated. * @param duration The duration of the animation. * @param makeVisibleAfterAnimation If {@code true}, the view will become visible after the - * animation ends. + * animation ends. */ - public static Animator createFadeOutAnimator(final View view, long duration, - boolean makeVisibleAfterAnimation) { + public static Animator createFadeOutAnimator( + final View view, long duration, boolean makeVisibleAfterAnimation) { ObjectAnimator animator = ObjectAnimator.ofFloat(view, View.ALPHA, 1.0f, 0.0f).setDuration(duration); if (makeVisibleAfterAnimation) { - animator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - view.setAlpha(1.0f); - } - }); + animator.addListener( + new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + view.setAlpha(1.0f); + } + }); } return animator; } diff --git a/common/src/com/android/tv/common/ui/setup/animation/TranslationAnimationCreator.java b/common/src/com/android/tv/common/ui/setup/animation/TranslationAnimationCreator.java index 99b8811a..13b89ea1 100644 --- a/common/src/com/android/tv/common/ui/setup/animation/TranslationAnimationCreator.java +++ b/common/src/com/android/tv/common/ui/setup/animation/TranslationAnimationCreator.java @@ -1,3 +1,18 @@ +/* + * Copyright (C) 2017 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. + */ package com.android.tv.common.ui.setup.animation; import android.animation.Animator; @@ -5,18 +20,16 @@ import android.animation.AnimatorListenerAdapter; import android.animation.ObjectAnimator; import android.animation.TimeInterpolator; import android.graphics.Path; +import android.support.v17.leanback.R; import android.transition.Transition; import android.transition.TransitionValues; import android.view.View; -import com.android.tv.common.R; - /** * This class is used by Slide and Explode to create an animator that goes from the start position * to the end position. It takes into account the canceled position so that it will not blink out or - * shift suddenly when the transition is interrupted. - * The original class is android.support.v17.leanback.transition.TranslationAnimationCreator which - * is hidden. + * shift suddenly when the transition is interrupted. The original class is + * android.support.v17.leanback.transition.TranslationAnimationCreator which is hidden. */ // Copied from android.support.v17.leanback.transition.TransltaionAnimationCreator class TranslationAnimationCreator { @@ -31,11 +44,16 @@ class TranslationAnimationCreator { * @param endX The end translation x of view * @param interpolator The interpolator to use with this animator. * @return An animator that moves from (startX, startY) to (endX, endY) unless there was a - * previous interruption, in which case it moves from the current position to (endX, - * endY). + * previous interruption, in which case it moves from the current position to (endX, endY). */ - static Animator createAnimation(View view, TransitionValues values, int viewPosX, float startX, - float endX, TimeInterpolator interpolator, Transition transition) { + static Animator createAnimation( + View view, + TransitionValues values, + int viewPosX, + float startX, + float endX, + TimeInterpolator interpolator, + Transition transition) { float terminalX = view.getTranslationX(); Integer startPosition = (Integer) values.view.getTag(R.id.transitionPosition); if (startPosition != null) { @@ -74,8 +92,8 @@ class TranslationAnimationCreator { private float mPausedX; private final float mTerminalX; - private TransitionPositionListener(View movingView, View viewInHierarchy, int startX, - float terminalX) { + private TransitionPositionListener( + View movingView, View viewInHierarchy, int startX, float terminalX) { mMovingView = movingView; mViewInHierarchy = viewInHierarchy; mStartX = startX - Math.round(mMovingView.getTranslationX()); @@ -123,6 +141,4 @@ class TranslationAnimationCreator { @Override public void onTransitionResume(Transition transition) {} } - } - diff --git a/common/src/com/android/tv/common/AutoCloseableUtils.java b/common/src/com/android/tv/common/util/AutoCloseableUtils.java index ad364cc4..605715ef 100644 --- a/common/src/com/android/tv/common/AutoCloseableUtils.java +++ b/common/src/com/android/tv/common/util/AutoCloseableUtils.java @@ -14,17 +14,15 @@ * limitations under the License */ -package com.android.tv.common; +package com.android.tv.common.util; import android.util.Log; -/** - * Static utilities for AutoCloseable. - */ +/** Static utilities for AutoCloseable. */ public class AutoCloseableUtils { private static final String TAG = "AutoCloseableUtils"; - private AutoCloseableUtils() { } + private AutoCloseableUtils() {} public static void closeQuietly(AutoCloseable closeable) { try { diff --git a/common/src/com/android/tv/common/util/Clock.java b/common/src/com/android/tv/common/util/Clock.java new file mode 100644 index 00000000..cd6ede86 --- /dev/null +++ b/common/src/com/android/tv/common/util/Clock.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2014 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. + */ +package com.android.tv.common.util; + +import android.os.SystemClock; + +/** + * An interface through which system clocks can be read. The {@link #SYSTEM} implementation must be + * used for all non-test cases. + */ +public interface Clock { + /** + * Returns the current time in milliseconds since January 1, 1970 00:00:00.0 UTC. + * + * @see System#currentTimeMillis(). + */ + long currentTimeMillis(); + + /** + * Returns milliseconds since boot, including time spent in sleep. + * + * @see SystemClock#elapsedRealtime() + */ + long elapsedRealtime(); + + /** + * Returns milliseconds since boot, not counting time spent in deep sleep. + * + * @return milliseconds of non-sleep uptime since boot. + * @see SystemClock#uptimeMillis() + */ + long uptimeMillis(); + + /** + * Waits a given number of milliseconds (of uptimeMillis) before returning. + * + * @param ms to sleep before returning, in milliseconds of uptime. + * @see SystemClock#sleep(long) + */ + void sleep(long ms); + + /** The default implementation of Clock. */ + Clock SYSTEM = + new Clock() { + @Override + public long currentTimeMillis() { + return System.currentTimeMillis(); + } + + @Override + public long elapsedRealtime() { + return SystemClock.elapsedRealtime(); + } + + @Override + public void sleep(long ms) { + SystemClock.sleep(ms); + } + + @Override + public long uptimeMillis() { + return SystemClock.uptimeMillis(); + } + }; +} diff --git a/common/src/com/android/tv/common/CollectionUtils.java b/common/src/com/android/tv/common/util/CollectionUtils.java index 300ad8f2..8ca7e3cc 100644 --- a/common/src/com/android/tv/common/CollectionUtils.java +++ b/common/src/com/android/tv/common/util/CollectionUtils.java @@ -14,7 +14,7 @@ * limitations under the License */ -package com.android.tv.common; +package com.android.tv.common.util; import java.util.ArrayList; import java.util.Arrays; @@ -23,16 +23,14 @@ import java.util.Collections; import java.util.Comparator; import java.util.List; -/** - * Static utilities for collections - */ +/** Static utilities for collections */ public class CollectionUtils { /** * Returns an array with the arrays concatenated together. * - * @see <a href="http://stackoverflow.com/a/784842/1122089">Stackoverflow answer</a> by - * <a href="http://stackoverflow.com/users/40342/joachim-sauer">Joachim Sauer</a> + * @see <a href="http://stackoverflow.com/a/784842/1122089">Stackoverflow answer</a> by <a + * href="http://stackoverflow.com/users/40342/joachim-sauer">Joachim Sauer</a> */ public static <T> T[] concatAll(T[] first, T[]... rest) { int totalLength = first.length; @@ -50,12 +48,12 @@ public class CollectionUtils { /** * Unions the two collections and returns the unified list. - * <p> - * The elements is not compared with hashcode() or equals(). Comparator is used for the equality - * check. + * + * <p>The elements is not compared with hashcode() or equals(). Comparator is used for the + * equality check. */ - public static <T> List<T> union(Collection<T> originals, Collection<T> toAdds, - Comparator<T> comparator) { + public static <T> List<T> union( + Collection<T> originals, Collection<T> toAdds, Comparator<T> comparator) { List<T> result = new ArrayList<>(originals); Collections.sort(result, comparator); List<T> resultToAdd = new ArrayList<>(); @@ -68,11 +66,9 @@ public class CollectionUtils { return result; } - /** - * Subtracts the elements from the original collection. - */ - public static <T> List<T> subtract(Collection<T> originals, T[] toSubtracts, - Comparator<T> comparator) { + /** Subtracts the elements from the original collection. */ + public static <T> List<T> subtract( + Collection<T> originals, T[] toSubtracts, Comparator<T> comparator) { List<T> result = new ArrayList<>(originals); Collections.sort(result, comparator); for (T toSubtract : toSubtracts) { @@ -84,11 +80,9 @@ public class CollectionUtils { return result; } - /** - * Returns {@code true} if the two specified collections have common elements. - */ - public static <T> boolean containsAny(Collection<T> c1, Collection<T> c2, - Comparator<T> comparator) { + /** Returns {@code true} if the two specified collections have common elements. */ + public static <T> boolean containsAny( + Collection<T> c1, Collection<T> c2, Comparator<T> comparator) { List<T> contains = new ArrayList<>(c1); Collections.sort(contains, comparator); for (T iterate : c2) { diff --git a/common/src/com/android/tv/common/util/CommonUtils.java b/common/src/com/android/tv/common/util/CommonUtils.java new file mode 100644 index 00000000..305431d3 --- /dev/null +++ b/common/src/com/android/tv/common/util/CommonUtils.java @@ -0,0 +1,154 @@ +/* + * Copyright 2015 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. + */ + +package com.android.tv.common.util; + +import android.content.Context; +import android.content.Intent; +import android.media.tv.TvInputInfo; +import android.os.Build; +import android.util.ArraySet; +import android.util.Log; +import com.android.tv.common.BuildConfig; +import com.android.tv.common.CommonConstants; +import com.android.tv.common.actions.InputSetupActionUtils; +import com.android.tv.common.experiments.Experiments; +import java.io.File; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import java.util.Set; + +/** Util class for common use in TV app and inputs. */ +@SuppressWarnings("AndroidApiChecker") // TODO(b/32513850) remove when error prone is updated +public final class CommonUtils { + private static final String TAG = "CommonUtils"; + private static final ThreadLocal<SimpleDateFormat> ISO_8601 = + new ThreadLocal() { + private final SimpleDateFormat value = + new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.US); + + @Override + protected SimpleDateFormat initialValue() { + return value; + } + }; + // Hardcoded list for known bundled inputs not written by OEM/SOCs. + // Bundled (system) inputs not in the list will get the high priority + // so they and their channels come first in the UI. + private static final Set<String> BUNDLED_PACKAGE_SET = new ArraySet<>(); + + static { + BUNDLED_PACKAGE_SET.add("com.android.tv"); + } + + private static Boolean sRunningInTest; + + private CommonUtils() {} + + /** + * Returns an intent to start the setup activity for the TV input using {@link + * InputSetupActionUtils#INTENT_ACTION_INPUT_SETUP}. + */ + public static Intent createSetupIntent(Intent originalSetupIntent, String inputId) { + if (originalSetupIntent == null) { + return null; + } + Intent setupIntent = new Intent(originalSetupIntent); + if (!InputSetupActionUtils.hasInputSetupAction(originalSetupIntent)) { + Intent intentContainer = new Intent(InputSetupActionUtils.INTENT_ACTION_INPUT_SETUP); + intentContainer.putExtra(InputSetupActionUtils.EXTRA_SETUP_INTENT, originalSetupIntent); + intentContainer.putExtra(InputSetupActionUtils.EXTRA_INPUT_ID, inputId); + setupIntent = intentContainer; + } + return setupIntent; + } + + /** + * Returns an intent to start the setup activity for this TV input using {@link + * InputSetupActionUtils#INTENT_ACTION_INPUT_SETUP}. + */ + public static Intent createSetupIntent(TvInputInfo input) { + return createSetupIntent(input.createSetupIntent(), input.getId()); + } + + /** + * Checks if this application is running in tests. + * + * <p>{@link android.app.ActivityManager#isRunningInTestHarness} doesn't return {@code true} for + * the usual devices even the application is running in tests. We need to figure it out by + * checking whether the class in tv-tests-common module can be loaded or not. + */ + public static synchronized boolean isRunningInTest() { + if (sRunningInTest == null) { + try { + Class.forName("com.android.tv.testing.utils.Utils"); + Log.i( + TAG, + "Assumed to be running in a test because" + + " com.android.tv.testing.utils.Utils is found"); + sRunningInTest = true; + } catch (ClassNotFoundException e) { + sRunningInTest = false; + } + } + return sRunningInTest; + } + + /** Checks whether a given package is in our bundled package set. */ + public static boolean isInBundledPackageSet(String packageName) { + return BUNDLED_PACKAGE_SET.contains(packageName); + } + + /** Checks whether a given input is a bundled input. */ + public static boolean isBundledInput(String inputId) { + for (String prefix : BUNDLED_PACKAGE_SET) { + if (inputId.startsWith(prefix + "/")) { + return true; + } + } + return false; + } + + /** Returns true if the application is packaged with Live TV. */ + public static boolean isPackagedWithLiveChannels(Context context) { + return (CommonConstants.BASE_PACKAGE.equals(context.getPackageName())); + } + + /** Returns true if the current user is a developer. */ + public static boolean isDeveloper() { + return BuildConfig.ENG || Experiments.ENABLE_DEVELOPER_FEATURES.get(); + } + + /** Converts time in milliseconds to a ISO 8061 string. */ + public static String toIsoDateTimeString(long timeMillis) { + return ISO_8601.get().format(new Date(timeMillis)); + } + + /** Deletes a file or a directory. */ + public static void deleteDirOrFile(File fileOrDirectory) { + if (fileOrDirectory.isDirectory()) { + for (File child : fileOrDirectory.listFiles()) { + deleteDirOrFile(child); + } + } + fileOrDirectory.delete(); + } + + public static boolean isRoboTest() { + return "robolectric".equals(Build.FINGERPRINT); + } +} diff --git a/common/src/com/android/tv/common/util/ContentUriUtils.java b/common/src/com/android/tv/common/util/ContentUriUtils.java new file mode 100644 index 00000000..6cbe5e12 --- /dev/null +++ b/common/src/com/android/tv/common/util/ContentUriUtils.java @@ -0,0 +1,45 @@ +/* + * Copyright 2018 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. + */ + +package com.android.tv.common.util; + +import android.content.ContentUris; +import android.net.Uri; +import android.util.Log; + +/** Static utils for{@link android.content.ContentUris}. */ +public class ContentUriUtils { + private static final String TAG = "ContentUriUtils"; + + /** + * Converts the last path segment to a long. + * + * <p>This supports a common convention for content URIs where an ID is stored in the last + * segment. + * + * @return the long conversion of the last segment or -1 if the path is empty or there is any + * error + * @see ContentUris#parseId(Uri) + */ + public static long safeParseId(Uri uri) { + try { + return ContentUris.parseId(uri); + } catch (Exception e) { + Log.d(TAG, "Error parsing " + uri, e); + return -1; + } + } +} diff --git a/common/src/com/android/tv/common/util/Debug.java b/common/src/com/android/tv/common/util/Debug.java new file mode 100644 index 00000000..ab908741 --- /dev/null +++ b/common/src/com/android/tv/common/util/Debug.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2016 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. + */ + +package com.android.tv.common.util; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +/** A class only for help developers. */ +public class Debug { + /** + * A threshold of start up time, when the start up time of Live TV is more than it, a + * warning will show to the developer. + */ + public static final long TIME_START_UP_DURATION_THRESHOLD = TimeUnit.SECONDS.toMillis(6); + /** Tag for measuring start up time of Live TV. */ + public static final String TAG_START_UP_TIMER = "start_up_timer"; + + /** A global map for duration timers. */ + private static final Map<String, DurationTimer> sTimerMap = new HashMap<>(); + + /** Returns the global duration timer by tag. */ + public static DurationTimer getTimer(String tag) { + if (sTimerMap.get(tag) != null) { + return sTimerMap.get(tag); + } + DurationTimer timer = new DurationTimer(tag, true); + sTimerMap.put(tag, timer); + return timer; + } + + /** Removes the global duration timer by tag. */ + public static DurationTimer removeTimer(String tag) { + return sTimerMap.remove(tag); + } +} diff --git a/common/src/com/android/tv/common/util/DurationTimer.java b/common/src/com/android/tv/common/util/DurationTimer.java new file mode 100644 index 00000000..91581ad5 --- /dev/null +++ b/common/src/com/android/tv/common/util/DurationTimer.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2015 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. + */ + +package com.android.tv.common.util; + +import android.os.SystemClock; +import android.util.Log; +import com.android.tv.common.BuildConfig; + +/** Times a duration. */ +public final class DurationTimer { + private static final String TAG = "DurationTimer"; + public static final long TIME_NOT_SET = -1; + + private long mStartTimeMs = TIME_NOT_SET; + private String mTag = TAG; + private boolean mLogEngOnly; + + public DurationTimer() {} + + public DurationTimer(String tag, boolean logEngOnly) { + mTag = tag; + mLogEngOnly = logEngOnly; + } + + /** Returns true if the timer is running. */ + public boolean isRunning() { + return mStartTimeMs != TIME_NOT_SET; + } + + /** Start the timer. */ + public void start() { + mStartTimeMs = SystemClock.elapsedRealtime(); + } + + /** Returns true if timer is started. */ + public boolean isStarted() { + return mStartTimeMs != TIME_NOT_SET; + } + + /** + * Returns the current duration in milliseconds or {@link #TIME_NOT_SET} if the timer is not + * running. + */ + public long getDuration() { + return isRunning() ? SystemClock.elapsedRealtime() - mStartTimeMs : TIME_NOT_SET; + } + + /** + * Stops the timer and resets its value to {@link #TIME_NOT_SET}. + * + * @return the current duration in milliseconds or {@link #TIME_NOT_SET} if the timer is not + * running. + */ + public long reset() { + long duration = getDuration(); + mStartTimeMs = TIME_NOT_SET; + return duration; + } + + /** Adds information and duration time to the log. */ + public void log(String message) { + if (isRunning() && (!mLogEngOnly || BuildConfig.ENG)) { + Log.i(mTag, message + " : " + getDuration() + "ms"); + } + } +} diff --git a/common/src/com/android/tv/common/util/LocationUtils.java b/common/src/com/android/tv/common/util/LocationUtils.java new file mode 100644 index 00000000..53155298 --- /dev/null +++ b/common/src/com/android/tv/common/util/LocationUtils.java @@ -0,0 +1,143 @@ +/* + * Copyright (C) 2016 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. + */ + +package com.android.tv.common.util; + +import android.Manifest; +import android.content.Context; +import android.content.pm.PackageManager; +import android.location.Address; +import android.location.Geocoder; +import android.location.Location; +import android.location.LocationListener; +import android.location.LocationManager; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.text.TextUtils; +import android.util.Log; +import com.android.tv.common.BuildConfig; + + + + +import java.io.IOException; +import java.util.List; +import java.util.Locale; + +/** A utility class to get the current location. */ +public class LocationUtils { + private static final String TAG = "LocationUtils"; + private static final boolean DEBUG = false; + + private static Context sApplicationContext; + private static Address sAddress; + private static String sCountry; + private static IOException sError; + + /** Checks the current location. */ + public static synchronized Address getCurrentAddress(Context context) + throws IOException, SecurityException { + if (sAddress != null) { + return sAddress; + } + if (sError != null) { + throw sError; + } + if (sApplicationContext == null) { + sApplicationContext = context.getApplicationContext(); + } + LocationUtilsHelper.startLocationUpdates(); + return null; + } + + /** Returns the current country. */ + @NonNull + public static synchronized String getCurrentCountry(Context context) { + if (sCountry != null) { + return sCountry; + } + if (TextUtils.isEmpty(sCountry)) { + sCountry = context.getResources().getConfiguration().locale.getCountry(); + } + return sCountry; + } + + private static void updateAddress(Location location) { + if (DEBUG) Log.d(TAG, "Updating address with " + location); + if (location == null) { + return; + } + Geocoder geocoder = new Geocoder(sApplicationContext, Locale.getDefault()); + try { + List<Address> addresses = + geocoder.getFromLocation(location.getLatitude(), location.getLongitude(), 1); + if (addresses != null && !addresses.isEmpty()) { + sAddress = addresses.get(0); + if (DEBUG) Log.d(TAG, "Got " + sAddress); + try { + PostalCodeUtils.updatePostalCode(sApplicationContext); + } catch (Exception e) { + // Do nothing + } + } else { + if (DEBUG) Log.d(TAG, "No address returned"); + } + sError = null; + } catch (IOException e) { + Log.w(TAG, "Error in updating address", e); + sError = e; + } + } + + private LocationUtils() {} + + private static class LocationUtilsHelper { + private static final LocationListener LOCATION_LISTENER = + new LocationListener() { + @Override + public void onLocationChanged(Location location) { + updateAddress(location); + } + + @Override + public void onStatusChanged(String provider, int status, Bundle extras) {} + + @Override + public void onProviderEnabled(String provider) {} + + @Override + public void onProviderDisabled(String provider) {} + }; + + private static LocationManager sLocationManager; + + public static void startLocationUpdates() { + if (sLocationManager == null) { + sLocationManager = + (LocationManager) + sApplicationContext.getSystemService(Context.LOCATION_SERVICE); + try { + sLocationManager.requestLocationUpdates( + LocationManager.NETWORK_PROVIDER, 1000, 10, LOCATION_LISTENER, null); + } catch (SecurityException e) { + // Enables requesting the location updates again. + sLocationManager = null; + throw e; + } + } + } + } +} diff --git a/common/src/com/android/tv/common/util/NetworkTrafficTags.java b/common/src/com/android/tv/common/util/NetworkTrafficTags.java new file mode 100644 index 00000000..91f2bcd1 --- /dev/null +++ b/common/src/com/android/tv/common/util/NetworkTrafficTags.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2017 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 + */ + +package com.android.tv.common.util; + +import android.net.TrafficStats; +import android.support.annotation.NonNull; +import java.util.concurrent.Executor; + +/** Constants for tagging network traffic in the Live channels app. */ +public final class NetworkTrafficTags { + + public static final int DEFAULT_LIVE_CHANNELS = 1; + public static final int LOGO_FETCHER = 2; + public static final int HDHOMERUN = 3; + public static final int EPG_FETCH = 4; + + /** + * An executor which simply wraps a provided delegate executor, but calls {@link + * TrafficStats#setThreadStatsTag(int)} before executing any task. + */ + public static class TrafficStatsTaggingExecutor implements Executor { + private final Executor delegateExecutor; + private final int tag; + + public TrafficStatsTaggingExecutor(Executor delegateExecutor, int tag) { + this.delegateExecutor = delegateExecutor; + this.tag = tag; + } + + @Override + public void execute(final @NonNull Runnable command) { + // TODO(b/62038127): robolectric does not support lamdas in unbundled apps + delegateExecutor.execute( + new Runnable() { + @Override + public void run() { + TrafficStats.setThreadStatsTag(tag); + try { + command.run(); + } finally { + TrafficStats.clearThreadStatsTag(); + } + } + }); + } + } + + private NetworkTrafficTags() {} +} diff --git a/common/src/com/android/tv/common/util/PermissionUtils.java b/common/src/com/android/tv/common/util/PermissionUtils.java new file mode 100644 index 00000000..8d409e50 --- /dev/null +++ b/common/src/com/android/tv/common/util/PermissionUtils.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2017 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. + */ +package com.android.tv.common.util; + +import android.content.Context; +import android.content.pm.PackageManager; + +/** Util class to handle permissions. */ +public class PermissionUtils { + /** Permission to read the TV listings. */ + public static final String PERMISSION_READ_TV_LISTINGS = "android.permission.READ_TV_LISTINGS"; + + private static Boolean sHasAccessAllEpgPermission; + private static Boolean sHasAccessWatchedHistoryPermission; + private static Boolean sHasModifyParentalControlsPermission; + + public static boolean hasAccessAllEpg(Context context) { + if (sHasAccessAllEpgPermission == null) { + sHasAccessAllEpgPermission = + context.checkSelfPermission( + "com.android.providers.tv.permission.ACCESS_ALL_EPG_DATA") + == PackageManager.PERMISSION_GRANTED; + } + return sHasAccessAllEpgPermission; + } + + public static boolean hasAccessWatchedHistory(Context context) { + if (sHasAccessWatchedHistoryPermission == null) { + sHasAccessWatchedHistoryPermission = + context.checkSelfPermission( + "com.android.providers.tv.permission.ACCESS_WATCHED_PROGRAMS") + == PackageManager.PERMISSION_GRANTED; + } + return sHasAccessWatchedHistoryPermission; + } + + public static boolean hasModifyParentalControls(Context context) { + if (sHasModifyParentalControlsPermission == null) { + sHasModifyParentalControlsPermission = + context.checkSelfPermission("android.permission.MODIFY_PARENTAL_CONTROLS") + == PackageManager.PERMISSION_GRANTED; + } + return sHasModifyParentalControlsPermission; + } + + public static boolean hasReadTvListings(Context context) { + return context.checkSelfPermission(PERMISSION_READ_TV_LISTINGS) + == PackageManager.PERMISSION_GRANTED; + } + + public static boolean hasInternet(Context context) { + return context.checkSelfPermission("android.permission.INTERNET") + == PackageManager.PERMISSION_GRANTED; + } +} diff --git a/common/src/com/android/tv/common/util/PostalCodeUtils.java b/common/src/com/android/tv/common/util/PostalCodeUtils.java new file mode 100644 index 00000000..c0917af2 --- /dev/null +++ b/common/src/com/android/tv/common/util/PostalCodeUtils.java @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2017 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. + */ + +package com.android.tv.common.util; + +import android.content.Context; +import android.location.Address; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.text.TextUtils; +import android.util.Log; +import com.android.tv.common.CommonPreferences; +import java.io.IOException; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.regex.Pattern; + +/** A utility class to update, get, and set the last known postal or zip code. */ +public class PostalCodeUtils { + private static final String TAG = "PostalCodeUtils"; + + // Postcode formats, where A signifies a letter and 9 a digit: + // US zip code format: 99999 + private static final String POSTCODE_REGEX_US = "^(\\d{5})"; + // UK postcode district formats: A9, A99, AA9, AA99 + // Full UK postcode format: Postcode District + space + 9AA + // Should be able to handle both postcode district and full postcode + private static final String POSTCODE_REGEX_GB = + "^([A-Z][A-Z]?[0-9][0-9A-Z]?)( ?[0-9][A-Z]{2})?$"; + private static final String POSTCODE_REGEX_GB_GIR = "^GIR( ?0AA)?$"; // special UK postcode + + private static final Map<String, Pattern> REGION_PATTERN = new HashMap<>(); + private static final Map<String, Integer> REGION_MAX_LENGTH = new HashMap<>(); + + static { + REGION_PATTERN.put(Locale.US.getCountry(), Pattern.compile(POSTCODE_REGEX_US)); + REGION_PATTERN.put( + Locale.UK.getCountry(), + Pattern.compile(POSTCODE_REGEX_GB + "|" + POSTCODE_REGEX_GB_GIR)); + REGION_MAX_LENGTH.put(Locale.US.getCountry(), 5); + REGION_MAX_LENGTH.put(Locale.UK.getCountry(), 8); + } + + // The longest postcode number is 10-character-long. + // Use a larger number to accommodate future changes. + private static final int DEFAULT_MAX_LENGTH = 16; + + /** Returns {@code true} if postal code has been changed */ + public static boolean updatePostalCode(Context context) + throws IOException, SecurityException, NoPostalCodeException { + String postalCode = getPostalCode(context); + String lastPostalCode = getLastPostalCode(context); + if (TextUtils.isEmpty(postalCode)) { + if (TextUtils.isEmpty(lastPostalCode)) { + throw new NoPostalCodeException(); + } + } else if (!TextUtils.equals(postalCode, lastPostalCode)) { + setLastPostalCode(context, postalCode); + return true; + } + return false; + } + + /** + * Gets the last stored postal or zip code, which might be decided by {@link LocationUtils} or + * input by users. + */ + public static String getLastPostalCode(Context context) { + return CommonPreferences.getLastPostalCode(context); + } + + /** + * Sets the last stored postal or zip code. This method will overwrite the value written by + * calling {@link #updatePostalCode(Context)}. + */ + public static void setLastPostalCode(Context context, String postalCode) { + Log.i(TAG, "Set Postal Code:" + postalCode); + CommonPreferences.setLastPostalCode(context, postalCode); + } + + @Nullable + private static String getPostalCode(Context context) throws IOException, SecurityException { + Address address = LocationUtils.getCurrentAddress(context); + if (address != null) { + Log.i( + TAG, + "Current country and postal code is " + + address.getCountryName() + + ", " + + address.getPostalCode()); + return address.getPostalCode(); + } + return null; + } + + /** An {@link java.lang.Exception} class to notify no valid postal or zip code is available. */ + public static class NoPostalCodeException extends Exception { + public NoPostalCodeException() {} + } + + /** + * Checks whether a postcode matches the format of the specific region. + * + * @return {@code false} if the region is supported and the postcode doesn't match; {@code true} + * otherwise + */ + public static boolean matches(@NonNull CharSequence postcode, @NonNull String region) { + Pattern pattern = REGION_PATTERN.get(region.toUpperCase()); + return pattern == null || pattern.matcher(postcode).matches(); + } + + /** + * Gets the largest possible postcode length in the region. + * + * @return maximum postcode length if the region is supported; {@link #DEFAULT_MAX_LENGTH} + * otherwise + */ + public static int getRegionMaxLength(Context context) { + Integer maxLength = + REGION_MAX_LENGTH.get(LocationUtils.getCurrentCountry(context).toUpperCase()); + return maxLength == null ? DEFAULT_MAX_LENGTH : maxLength; + } +} diff --git a/common/src/com/android/tv/common/SharedPreferencesUtils.java b/common/src/com/android/tv/common/util/SharedPreferencesUtils.java index 140c4e6f..e8bfe61b 100644 --- a/common/src/com/android/tv/common/SharedPreferencesUtils.java +++ b/common/src/com/android/tv/common/util/SharedPreferencesUtils.java @@ -14,15 +14,14 @@ * limitations under the License */ -package com.android.tv.common; +package com.android.tv.common.util; import android.content.Context; import android.os.AsyncTask; import android.preference.PreferenceManager; +import com.android.tv.common.CommonConstants; -/** - * Static utilities for {@link android.content.SharedPreferences} - */ +/** Static utilities for {@link android.content.SharedPreferences} */ public final class SharedPreferencesUtils { // Note that changing the preference name will reset the preference values. public static final String SHARED_PREF_FEATURES = "sharePreferencesFeatures"; @@ -31,7 +30,7 @@ public final class SharedPreferencesUtils { public static final String SHARED_PREF_DVR_WATCHED_POSITION = "dvr_watched_position_shared_preference"; public static final String SHARED_PREF_AUDIO_CAPABILITIES = - "com.android.tv.audio_capabilities"; + CommonConstants.BASE_PACKAGE + ".audio_capabilities"; public static final String SHARED_PREF_RECURRING_RUNNER = "sharedPreferencesRecurringRunner"; public static final String SHARED_PREF_EPG = "epg_preferences"; public static final String SHARED_PREF_SERIES_RECORDINGS = "seriesRecordings"; @@ -39,15 +38,16 @@ public final class SharedPreferencesUtils { public static final String SHARED_PREF_CHANNEL_LOGO_URIS = "channelLogoUris"; /** Stores the UI related settings */ public static final String SHARED_PREF_UI_SETTINGS = "ui_settings"; + public static final String SHARED_PREF_PREVIEW_PROGRAMS = "previewPrograms"; private static boolean sInitializeCalled; /** - * {@link android.content.SharedPreferences} loads the preference file when - * {@link Context#getSharedPreferences(String, int)} is called for the first time. - * Call {@link Context#getSharedPreferences(String, int)} as early as possible to avoid the ANR - * due to the file loading. + * {@link android.content.SharedPreferences} loads the preference file when {@link + * Context#getSharedPreferences(String, int)} is called for the first time. Call {@link + * Context#getSharedPreferences(String, int)} as early as possible to avoid the ANR due to the + * file loading. */ public static synchronized void initialize(final Context context, final Runnable postTask) { if (!sInitializeCalled) { @@ -59,15 +59,15 @@ public final class SharedPreferencesUtils { context.getSharedPreferences(SHARED_PREF_FEATURES, Context.MODE_PRIVATE); context.getSharedPreferences(SHARED_PREF_BROWSABLE, Context.MODE_PRIVATE); context.getSharedPreferences(SHARED_PREF_WATCHED_HISTORY, Context.MODE_PRIVATE); - context.getSharedPreferences(SHARED_PREF_DVR_WATCHED_POSITION, - Context.MODE_PRIVATE); - context.getSharedPreferences(SHARED_PREF_AUDIO_CAPABILITIES, - Context.MODE_PRIVATE); - context.getSharedPreferences(SHARED_PREF_RECURRING_RUNNER, - Context.MODE_PRIVATE); + context.getSharedPreferences( + SHARED_PREF_DVR_WATCHED_POSITION, Context.MODE_PRIVATE); + context.getSharedPreferences( + SHARED_PREF_AUDIO_CAPABILITIES, Context.MODE_PRIVATE); + context.getSharedPreferences( + SHARED_PREF_RECURRING_RUNNER, Context.MODE_PRIVATE); context.getSharedPreferences(SHARED_PREF_EPG, Context.MODE_PRIVATE); - context.getSharedPreferences(SHARED_PREF_SERIES_RECORDINGS, - Context.MODE_PRIVATE); + context.getSharedPreferences( + SHARED_PREF_SERIES_RECORDINGS, Context.MODE_PRIVATE); context.getSharedPreferences(SHARED_PREF_UI_SETTINGS, Context.MODE_PRIVATE); return null; } @@ -80,5 +80,5 @@ public final class SharedPreferencesUtils { } } - private SharedPreferencesUtils() { } + private SharedPreferencesUtils() {} } diff --git a/common/src/com/android/tv/common/util/StringUtils.java b/common/src/com/android/tv/common/util/StringUtils.java new file mode 100644 index 00000000..b9461426 --- /dev/null +++ b/common/src/com/android/tv/common/util/StringUtils.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2015 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. + */ + +package com.android.tv.common.util; + +/** Utility class for handling {@link String}. */ +public final class StringUtils { + + private StringUtils() {} + + /** Returns compares two strings lexicographically and handles null values quietly. */ + public static int compare(String a, String b) { + if (a == null) { + return b == null ? 0 : -1; + } + if (b == null) { + return 1; + } + return a.compareTo(b); + } +} diff --git a/common/src/com/android/tv/common/util/SystemProperties.java b/common/src/com/android/tv/common/util/SystemProperties.java new file mode 100644 index 00000000..a9f18d4b --- /dev/null +++ b/common/src/com/android/tv/common/util/SystemProperties.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2015 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. + */ + +package com.android.tv.common.util; + +import com.android.tv.common.BooleanSystemProperty; + +/** A convenience class for getting TV related system properties. */ +public final class SystemProperties { + + /** Allow Google Analytics for eng builds. */ + public static final BooleanSystemProperty ALLOW_ANALYTICS_IN_ENG = + new BooleanSystemProperty("tv_allow_analytics_in_eng", false); + + /** Allow Strict mode for debug builds. */ + public static final BooleanSystemProperty ALLOW_STRICT_MODE = + new BooleanSystemProperty("tv_allow_strict_mode", true); + + /** When true {@link android.view.KeyEvent}s are logged. Defaults to false. */ + public static final BooleanSystemProperty LOG_KEYEVENT = + new BooleanSystemProperty("tv_log_keyevent", false); + /** When true debug keys are used. Defaults to false. */ + public static final BooleanSystemProperty USE_DEBUG_KEYS = + new BooleanSystemProperty("tv_use_debug_keys", false); + + /** Send {@link com.android.tv.analytics.Tracker} information. Defaults to {@code true}. */ + public static final BooleanSystemProperty USE_TRACKER = + new BooleanSystemProperty("tv_use_tracker", true); + + static { + updateSystemProperties(); + } + + private SystemProperties() {} + + /** Update the TV related system properties. */ + public static void updateSystemProperties() { + BooleanSystemProperty.resetAll(); + } +} diff --git a/common/src/com/android/tv/common/util/SystemPropertiesProxy.java b/common/src/com/android/tv/common/util/SystemPropertiesProxy.java new file mode 100644 index 00000000..a3ffd0fa --- /dev/null +++ b/common/src/com/android/tv/common/util/SystemPropertiesProxy.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2015 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. + */ + +package com.android.tv.common.util; + +import android.util.Log; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +/** + * Proxy class that gives an access to a hidden API {@link android.os.SystemProperties#getBoolean}. + */ +public class SystemPropertiesProxy { + private static final String TAG = "SystemPropertiesProxy"; + + private SystemPropertiesProxy() {} + + public static boolean getBoolean(String key, boolean def) throws IllegalArgumentException { + try { + Class SystemPropertiesClass = Class.forName("android.os.SystemProperties"); + Method getBooleanMethod = + SystemPropertiesClass.getDeclaredMethod( + "getBoolean", String.class, boolean.class); + getBooleanMethod.setAccessible(true); + return (boolean) getBooleanMethod.invoke(SystemPropertiesClass, key, def); + } catch (InvocationTargetException + | IllegalAccessException + | NoSuchMethodException + | ClassNotFoundException e) { + Log.e(TAG, "Failed to invoke SystemProperties.getBoolean()", e); + } + return def; + } + + public static int getInt(String key, int def) throws IllegalArgumentException { + try { + Class SystemPropertiesClass = Class.forName("android.os.SystemProperties"); + Method getIntMethod = + SystemPropertiesClass.getDeclaredMethod("getInt", String.class, int.class); + getIntMethod.setAccessible(true); + return (int) getIntMethod.invoke(SystemPropertiesClass, key, def); + } catch (InvocationTargetException + | IllegalAccessException + | NoSuchMethodException + | ClassNotFoundException e) { + Log.e(TAG, "Failed to invoke SystemProperties.getInt()", e); + } + return def; + } + + public static String getString(String key, String def) throws IllegalArgumentException { + try { + Class SystemPropertiesClass = Class.forName("android.os.SystemProperties"); + Method getIntMethod = + SystemPropertiesClass.getDeclaredMethod("get", String.class, String.class); + getIntMethod.setAccessible(true); + return (String) getIntMethod.invoke(SystemPropertiesClass, key, def); + } catch (InvocationTargetException + | IllegalAccessException + | NoSuchMethodException + | ClassNotFoundException e) { + Log.e(TAG, "Failed to invoke SystemProperties.get()", e); + } + return def; + } +} |