diff options
Diffstat (limited to 'mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/scheduler')
5 files changed, 637 insertions, 0 deletions
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/scheduler/PlatformScheduler.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/scheduler/PlatformScheduler.java new file mode 100644 index 0000000000..bb866944d4 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/scheduler/PlatformScheduler.java @@ -0,0 +1,150 @@ +/* + * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.scheduler; + +import android.annotation.TargetApi; +import android.app.job.JobInfo; +import android.app.job.JobParameters; +import android.app.job.JobScheduler; +import android.app.job.JobService; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.os.PersistableBundle; +import androidx.annotation.RequiresPermission; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; + +/** + * A {@link Scheduler} that uses {@link JobScheduler}. To use this scheduler, you must add {@link + * PlatformSchedulerService} to your manifest: + * + * <pre>{@literal + * <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/> + * <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/> + * + * <service android:name="com.google.android.exoplayer2.scheduler.PlatformScheduler$PlatformSchedulerService" + * android:permission="android.permission.BIND_JOB_SERVICE" + * android:exported="true"/> + * }</pre> + */ +@TargetApi(21) +public final class PlatformScheduler implements Scheduler { + + private static final boolean DEBUG = false; + private static final String TAG = "PlatformScheduler"; + private static final String KEY_SERVICE_ACTION = "service_action"; + private static final String KEY_SERVICE_PACKAGE = "service_package"; + private static final String KEY_REQUIREMENTS = "requirements"; + + private final int jobId; + private final ComponentName jobServiceComponentName; + private final JobScheduler jobScheduler; + + /** + * @param context Any context. + * @param jobId An identifier for the jobs scheduled by this instance. If the same identifier was + * used by a previous instance, anything scheduled by the previous instance will be canceled + * by this instance if {@link #schedule(Requirements, String, String)} or {@link #cancel()} + * are called. + */ + @RequiresPermission(android.Manifest.permission.RECEIVE_BOOT_COMPLETED) + public PlatformScheduler(Context context, int jobId) { + context = context.getApplicationContext(); + this.jobId = jobId; + jobServiceComponentName = new ComponentName(context, PlatformSchedulerService.class); + jobScheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE); + } + + @Override + public boolean schedule(Requirements requirements, String servicePackage, String serviceAction) { + JobInfo jobInfo = + buildJobInfo(jobId, jobServiceComponentName, requirements, serviceAction, servicePackage); + int result = jobScheduler.schedule(jobInfo); + logd("Scheduling job: " + jobId + " result: " + result); + return result == JobScheduler.RESULT_SUCCESS; + } + + @Override + public boolean cancel() { + logd("Canceling job: " + jobId); + jobScheduler.cancel(jobId); + return true; + } + + // @RequiresPermission constructor annotation should ensure the permission is present. + @SuppressWarnings("MissingPermission") + private static JobInfo buildJobInfo( + int jobId, + ComponentName jobServiceComponentName, + Requirements requirements, + String serviceAction, + String servicePackage) { + JobInfo.Builder builder = new JobInfo.Builder(jobId, jobServiceComponentName); + + if (requirements.isUnmeteredNetworkRequired()) { + builder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED); + } else if (requirements.isNetworkRequired()) { + builder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY); + } + builder.setRequiresDeviceIdle(requirements.isIdleRequired()); + builder.setRequiresCharging(requirements.isChargingRequired()); + builder.setPersisted(true); + + PersistableBundle extras = new PersistableBundle(); + extras.putString(KEY_SERVICE_ACTION, serviceAction); + extras.putString(KEY_SERVICE_PACKAGE, servicePackage); + extras.putInt(KEY_REQUIREMENTS, requirements.getRequirements()); + builder.setExtras(extras); + + return builder.build(); + } + + private static void logd(String message) { + if (DEBUG) { + Log.d(TAG, message); + } + } + + /** A {@link JobService} that starts the target service if the requirements are met. */ + public static final class PlatformSchedulerService extends JobService { + @Override + public boolean onStartJob(JobParameters params) { + logd("PlatformSchedulerService started"); + PersistableBundle extras = params.getExtras(); + Requirements requirements = new Requirements(extras.getInt(KEY_REQUIREMENTS)); + if (requirements.checkRequirements(this)) { + logd("Requirements are met"); + String serviceAction = extras.getString(KEY_SERVICE_ACTION); + String servicePackage = extras.getString(KEY_SERVICE_PACKAGE); + Intent intent = + new Intent(Assertions.checkNotNull(serviceAction)).setPackage(servicePackage); + logd("Starting service action: " + serviceAction + " package: " + servicePackage); + Util.startForegroundService(this, intent); + } else { + logd("Requirements are not met"); + jobFinished(params, /* needsReschedule */ true); + } + return false; + } + + @Override + public boolean onStopJob(JobParameters params) { + return false; + } + } +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/scheduler/Requirements.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/scheduler/Requirements.java new file mode 100644 index 0000000000..9ef8fdb3f6 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/scheduler/Requirements.java @@ -0,0 +1,223 @@ +/* + * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.scheduler; + +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.net.ConnectivityManager; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.net.NetworkInfo; +import android.os.BatteryManager; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.PowerManager; +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** Defines a set of device state requirements. */ +public final class Requirements implements Parcelable { + + /** + * Requirement flags. Possible flag values are {@link #NETWORK}, {@link #NETWORK_UNMETERED}, + * {@link #DEVICE_IDLE} and {@link #DEVICE_CHARGING}. + */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef( + flag = true, + value = {NETWORK, NETWORK_UNMETERED, DEVICE_IDLE, DEVICE_CHARGING}) + public @interface RequirementFlags {} + + /** Requirement that the device has network connectivity. */ + public static final int NETWORK = 1; + /** Requirement that the device has a network connection that is unmetered. */ + public static final int NETWORK_UNMETERED = 1 << 1; + /** Requirement that the device is idle. */ + public static final int DEVICE_IDLE = 1 << 2; + /** Requirement that the device is charging. */ + public static final int DEVICE_CHARGING = 1 << 3; + + @RequirementFlags private final int requirements; + + /** @param requirements A combination of requirement flags. */ + public Requirements(@RequirementFlags int requirements) { + if ((requirements & NETWORK_UNMETERED) != 0) { + // Make sure network requirement flags are consistent. + requirements |= NETWORK; + } + this.requirements = requirements; + } + + /** Returns the requirements. */ + @RequirementFlags + public int getRequirements() { + return requirements; + } + + /** Returns whether network connectivity is required. */ + public boolean isNetworkRequired() { + return (requirements & NETWORK) != 0; + } + + /** Returns whether un-metered network connectivity is required. */ + public boolean isUnmeteredNetworkRequired() { + return (requirements & NETWORK_UNMETERED) != 0; + } + + /** Returns whether the device is required to be charging. */ + public boolean isChargingRequired() { + return (requirements & DEVICE_CHARGING) != 0; + } + + /** Returns whether the device is required to be idle. */ + public boolean isIdleRequired() { + return (requirements & DEVICE_IDLE) != 0; + } + + /** + * Returns whether the requirements are met. + * + * @param context Any context. + * @return Whether the requirements are met. + */ + public boolean checkRequirements(Context context) { + return getNotMetRequirements(context) == 0; + } + + /** + * Returns requirements that are not met, or 0. + * + * @param context Any context. + * @return The requirements that are not met, or 0. + */ + @RequirementFlags + public int getNotMetRequirements(Context context) { + @RequirementFlags int notMetRequirements = getNotMetNetworkRequirements(context); + if (isChargingRequired() && !isDeviceCharging(context)) { + notMetRequirements |= DEVICE_CHARGING; + } + if (isIdleRequired() && !isDeviceIdle(context)) { + notMetRequirements |= DEVICE_IDLE; + } + return notMetRequirements; + } + + @RequirementFlags + private int getNotMetNetworkRequirements(Context context) { + if (!isNetworkRequired()) { + return 0; + } + + ConnectivityManager connectivityManager = + (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo networkInfo = Assertions.checkNotNull(connectivityManager).getActiveNetworkInfo(); + if (networkInfo == null + || !networkInfo.isConnected() + || !isInternetConnectivityValidated(connectivityManager)) { + return requirements & (NETWORK | NETWORK_UNMETERED); + } + + if (isUnmeteredNetworkRequired() && connectivityManager.isActiveNetworkMetered()) { + return NETWORK_UNMETERED; + } + + return 0; + } + + private boolean isDeviceCharging(Context context) { + Intent batteryStatus = + context.registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED)); + if (batteryStatus == null) { + return false; + } + int status = batteryStatus.getIntExtra(BatteryManager.EXTRA_STATUS, -1); + return status == BatteryManager.BATTERY_STATUS_CHARGING + || status == BatteryManager.BATTERY_STATUS_FULL; + } + + private boolean isDeviceIdle(Context context) { + PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE); + return Util.SDK_INT >= 23 + ? powerManager.isDeviceIdleMode() + : Util.SDK_INT >= 20 ? !powerManager.isInteractive() : !powerManager.isScreenOn(); + } + + private static boolean isInternetConnectivityValidated(ConnectivityManager connectivityManager) { + // It's possible to query NetworkCapabilities from API level 23, but RequirementsWatcher only + // fires an event to update its Requirements when NetworkCapabilities change from API level 24. + // Since Requirements won't be updated, we assume connectivity is validated on API level 23. + if (Util.SDK_INT < 24) { + return true; + } + Network activeNetwork = connectivityManager.getActiveNetwork(); + if (activeNetwork == null) { + return false; + } + NetworkCapabilities networkCapabilities = + connectivityManager.getNetworkCapabilities(activeNetwork); + return networkCapabilities != null + && networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED); + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + return requirements == ((Requirements) o).requirements; + } + + @Override + public int hashCode() { + return requirements; + } + + // Parcelable implementation. + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(requirements); + } + + public static final Parcelable.Creator<Requirements> CREATOR = + new Creator<Requirements>() { + + @Override + public Requirements createFromParcel(Parcel in) { + return new Requirements(in.readInt()); + } + + @Override + public Requirements[] newArray(int size) { + return new Requirements[size]; + } + }; +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/scheduler/RequirementsWatcher.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/scheduler/RequirementsWatcher.java new file mode 100644 index 0000000000..edb860ac05 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/scheduler/RequirementsWatcher.java @@ -0,0 +1,197 @@ +/* + * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.scheduler; + +import android.annotation.TargetApi; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.net.ConnectivityManager; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.os.Handler; +import android.os.Looper; +import android.os.PowerManager; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; + +/** + * Watches whether the {@link Requirements} are met and notifies the {@link Listener} on changes. + */ +public final class RequirementsWatcher { + + /** + * Notified when RequirementsWatcher instance first created and on changes whether the {@link + * Requirements} are met. + */ + public interface Listener { + /** + * Called when there is a change on the met requirements. + * + * @param requirementsWatcher Calling instance. + * @param notMetRequirements {@link Requirements.RequirementFlags RequirementFlags} that are not + * met, or 0. + */ + void onRequirementsStateChanged( + RequirementsWatcher requirementsWatcher, + @Requirements.RequirementFlags int notMetRequirements); + } + + private final Context context; + private final Listener listener; + private final Requirements requirements; + private final Handler handler; + + @Nullable private DeviceStatusChangeReceiver receiver; + + @Requirements.RequirementFlags private int notMetRequirements; + @Nullable private NetworkCallback networkCallback; + + /** + * @param context Any context. + * @param listener Notified whether the {@link Requirements} are met. + * @param requirements The requirements to watch. + */ + public RequirementsWatcher(Context context, Listener listener, Requirements requirements) { + this.context = context.getApplicationContext(); + this.listener = listener; + this.requirements = requirements; + handler = new Handler(Util.getLooper()); + } + + /** + * Starts watching for changes. Must be called from a thread that has an associated {@link + * Looper}. Listener methods are called on the caller thread. + * + * @return Initial {@link Requirements.RequirementFlags RequirementFlags} that are not met, or 0. + */ + @Requirements.RequirementFlags + public int start() { + notMetRequirements = requirements.getNotMetRequirements(context); + + IntentFilter filter = new IntentFilter(); + if (requirements.isNetworkRequired()) { + if (Util.SDK_INT >= 24) { + registerNetworkCallbackV24(); + } else { + filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION); + } + } + if (requirements.isChargingRequired()) { + filter.addAction(Intent.ACTION_POWER_CONNECTED); + filter.addAction(Intent.ACTION_POWER_DISCONNECTED); + } + if (requirements.isIdleRequired()) { + if (Util.SDK_INT >= 23) { + filter.addAction(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED); + } else { + filter.addAction(Intent.ACTION_SCREEN_ON); + filter.addAction(Intent.ACTION_SCREEN_OFF); + } + } + receiver = new DeviceStatusChangeReceiver(); + context.registerReceiver(receiver, filter, null, handler); + return notMetRequirements; + } + + /** Stops watching for changes. */ + public void stop() { + context.unregisterReceiver(Assertions.checkNotNull(receiver)); + receiver = null; + if (Util.SDK_INT >= 24 && networkCallback != null) { + unregisterNetworkCallbackV24(); + } + } + + /** Returns watched {@link Requirements}. */ + public Requirements getRequirements() { + return requirements; + } + + @TargetApi(24) + private void registerNetworkCallbackV24() { + ConnectivityManager connectivityManager = + Assertions.checkNotNull( + (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE)); + networkCallback = new NetworkCallback(); + connectivityManager.registerDefaultNetworkCallback(networkCallback); + } + + @TargetApi(24) + private void unregisterNetworkCallbackV24() { + ConnectivityManager connectivityManager = + (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + connectivityManager.unregisterNetworkCallback(Assertions.checkNotNull(networkCallback)); + networkCallback = null; + } + + private void checkRequirements() { + @Requirements.RequirementFlags + int notMetRequirements = requirements.getNotMetRequirements(context); + if (this.notMetRequirements != notMetRequirements) { + this.notMetRequirements = notMetRequirements; + listener.onRequirementsStateChanged(this, notMetRequirements); + } + } + + private class DeviceStatusChangeReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + if (!isInitialStickyBroadcast()) { + checkRequirements(); + } + } + } + + @RequiresApi(24) + private final class NetworkCallback extends ConnectivityManager.NetworkCallback { + boolean receivedCapabilitiesChange; + boolean networkValidated; + + @Override + public void onAvailable(Network network) { + onNetworkCallback(); + } + + @Override + public void onLost(Network network) { + onNetworkCallback(); + } + + @Override + public void onCapabilitiesChanged(Network network, NetworkCapabilities networkCapabilities) { + boolean networkValidated = + networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED); + if (!receivedCapabilitiesChange || this.networkValidated != networkValidated) { + receivedCapabilitiesChange = true; + this.networkValidated = networkValidated; + onNetworkCallback(); + } + } + + private void onNetworkCallback() { + handler.post( + () -> { + if (networkCallback != null) { + checkRequirements(); + } + }); + } + } +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/scheduler/Scheduler.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/scheduler/Scheduler.java new file mode 100644 index 0000000000..c7a7afcd2d --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/scheduler/Scheduler.java @@ -0,0 +1,48 @@ +/* + * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.scheduler; + +import android.app.Notification; +import android.app.Service; +import android.content.Intent; + +/** Schedules a service to be started in the foreground when some {@link Requirements} are met. */ +public interface Scheduler { + + /** + * Schedules a service to be started in the foreground when some {@link Requirements} are met. + * Anything that was previously scheduled will be canceled. + * + * <p>The service to be started must be declared in the manifest of {@code servicePackage} with an + * intent filter containing {@code serviceAction}. Note that when started with {@code + * serviceAction}, the service must call {@link Service#startForeground(int, Notification)} to + * make itself a foreground service, as documented by {@link + * Service#startForegroundService(Intent)}. + * + * @param requirements The requirements. + * @param servicePackage The package name. + * @param serviceAction The action with which the service will be started. + * @return Whether scheduling was successful. + */ + boolean schedule(Requirements requirements, String servicePackage, String serviceAction); + + /** + * Cancels anything that was previously scheduled, or else does nothing. + * + * @return Whether cancellation was successful. + */ + boolean cancel(); +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/scheduler/package-info.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/scheduler/package-info.java new file mode 100644 index 0000000000..b4e68ebfff --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/scheduler/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 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. + */ +@NonNullApi +package org.mozilla.thirdparty.com.google.android.exoplayer2.scheduler; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.NonNullApi; |